Build TravelRun intro and tutorial flow

This commit is contained in:
jongjae0305
2026-07-06 16:15:20 +09:00
parent af26dd4311
commit 2ba3882f1b
153 changed files with 13625 additions and 2086 deletions
+61
View File
@@ -4,13 +4,32 @@ using UnityEngine;
[ExecuteAlways]
public class BackgroundLoop : MonoBehaviour {
public Sprite[] backgroundSprites; // 순서대로 교체할 공항 배경 스프라이트
public bool useResourceBackground = true; // Resources 폴더의 맵 배경을 자동으로 사용
public string resourceBackgroundPath = ""; // 맵 데이터가 없을 때 사용할 Resources 배경
public int startSpriteIndex = 0; // 이 배경 오브젝트가 시작할 스프라이트 번호
public int spriteAdvanceOnLoop = 2; // 배경 오브젝트가 두 장이므로 재배치될 때 두 칸씩 진행
private static string activeResourceBackgroundPath = "";
private float width; // 배경의 가로 길이
private int currentSpriteIndex; // 현재 표시 중인 스프라이트 번호
private SpriteRenderer spriteRenderer; // 배경을 표시하는 스프라이트 렌더러
private BoxCollider2D backgroundCollider; // 루프 폭 계산용 콜라이더
private Sprite[] originalBackgroundSprites; // 맵 배경 리소스가 없을 때 되돌릴 기본 배경
private bool originalBackgroundSpritesCached = false;
public static void ApplyResourceBackground(string resourcePath) {
activeResourceBackgroundPath = resourcePath;
BackgroundLoop[] backgroundLoops = FindObjectsByType<BackgroundLoop>(FindObjectsSortMode.None);
for(int i = 0; i < backgroundLoops.Length; i++)
{
if(backgroundLoops[i] != null)
{
backgroundLoops[i].InitializeBackground();
}
}
}
private void Awake() {
InitializeBackground();
@@ -39,6 +58,8 @@ public class BackgroundLoop : MonoBehaviour {
private void InitializeBackground() {
CacheComponents();
CacheOriginalBackgroundSprites();
EnsureResourceBackground();
currentSpriteIndex = NormalizeSpriteIndex(startSpriteIndex);
ApplyBackgroundSprite(currentSpriteIndex);
@@ -58,6 +79,46 @@ public class BackgroundLoop : MonoBehaviour {
}
}
private void EnsureResourceBackground() {
if(!useResourceBackground)
{
return;
}
string finalResourcePath = !string.IsNullOrEmpty(activeResourceBackgroundPath)
? activeResourceBackgroundPath
: resourceBackgroundPath;
if(string.IsNullOrEmpty(finalResourcePath))
{
backgroundSprites = originalBackgroundSprites;
return;
}
Sprite resourceSprite = Resources.Load<Sprite>(finalResourcePath);
if(resourceSprite == null)
{
return;
}
if(backgroundSprites != null && backgroundSprites.Length == 1 && backgroundSprites[0] == resourceSprite)
{
return;
}
backgroundSprites = new Sprite[] { resourceSprite };
}
private void CacheOriginalBackgroundSprites() {
if(originalBackgroundSpritesCached)
{
return;
}
originalBackgroundSprites = backgroundSprites;
originalBackgroundSpritesCached = true;
}
private void RefreshWidth() {
if(spriteRenderer != null && spriteRenderer.sprite != null)
{
+200
View File
@@ -0,0 +1,200 @@
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
// 코스 데이터가 발판 위에 따로 얹는 장애물 프리팹을 제어한다.
public class CourseObstacle : MonoBehaviour {
public enum ObstacleKind {
Jump,
Slide
}
public ObstacleKind obstacleKind = ObstacleKind.Jump;
public Transform lowObstacle;
public SlideObstaclePattern slidePattern;
public Transform ceilingPlatform;
private Platform.PlatformSkin reservedSkin = Platform.PlatformSkin.Airport;
private Platform.PlatformPattern reservedPlatformPattern = Platform.PlatformPattern.LowMid;
private SlideObstaclePattern.SlideLayout reservedSlideLayout = SlideObstaclePattern.SlideLayout.CenterPair;
public void Configure(
Platform.PlatformSkin platformSkin,
Platform.PlatformPattern platformPattern,
SlideObstaclePattern.SlideLayout slideLayout) {
reservedSkin = platformSkin;
reservedPlatformPattern = platformPattern;
reservedSlideLayout = slideLayout;
ApplyConfiguration();
}
private void Awake() {
CacheReferences();
}
private void OnEnable() {
ApplyConfiguration();
}
private void CacheReferences() {
if(lowObstacle == null)
{
Transform foundObstacle = transform.Find("Jump Obstacle");
if(foundObstacle != null)
{
lowObstacle = foundObstacle;
}
}
if(slidePattern == null)
{
slidePattern = GetComponent<SlideObstaclePattern>();
}
if(ceilingPlatform == null)
{
Transform foundCeiling = transform.Find("Ceiling Platform");
if(foundCeiling != null)
{
ceilingPlatform = foundCeiling;
}
}
}
private void ApplyConfiguration() {
CacheReferences();
if(obstacleKind == ObstacleKind.Jump)
{
ApplyJumpPosition();
}
else if(slidePattern != null)
{
slidePattern.ConfigureLayout(reservedSlideLayout);
}
ApplyVisualSkin();
}
private void ApplyJumpPosition() {
if(lowObstacle == null)
{
return;
}
Vector3 localPosition = lowObstacle.localPosition;
localPosition.x = GetJumpObstacleX(reservedPlatformPattern);
lowObstacle.localPosition = localPosition;
}
private float GetJumpObstacleX(Platform.PlatformPattern platformPattern) {
switch(platformPattern)
{
case Platform.PlatformPattern.LowLeft:
return -0.44f;
case Platform.PlatformPattern.LowRight:
return 0.44f;
case Platform.PlatformPattern.LowMid:
default:
return 0f;
}
}
private void ApplyVisualSkin() {
#if UNITY_EDITOR
if(obstacleKind == ObstacleKind.Jump)
{
ApplySprite(lowObstacle, LoadSpriteAtPath(GetLowObstacleSkinPath(reservedSkin)));
return;
}
Sprite hangingSprite = LoadSpriteAtPath(GetHangingObstacleSkinPath(reservedSkin));
Sprite ceilingSprite = LoadSpriteAtPath(GetCeilingSkinPath(reservedSkin));
if(slidePattern != null && slidePattern.obstacles != null)
{
for(int i = 0; i < slidePattern.obstacles.Length; i++)
{
ApplySprite(slidePattern.obstacles[i], hangingSprite);
}
}
ApplySprite(ceilingPlatform, ceilingSprite);
#endif
}
#if UNITY_EDITOR
private void ApplySprite(Transform target, Sprite sprite) {
if(target == null || sprite == null)
{
return;
}
SpriteRenderer spriteRenderer = target.GetComponent<SpriteRenderer>();
if(spriteRenderer != null)
{
spriteRenderer.sprite = sprite;
}
}
private Sprite LoadSpriteAtPath(string assetPath) {
if(string.IsNullOrEmpty(assetPath))
{
return null;
}
return AssetDatabase.LoadAssetAtPath<Sprite>(assetPath);
}
private string GetLowObstacleSkinPath(Platform.PlatformSkin platformSkin) {
switch(platformSkin)
{
case Platform.PlatformSkin.Japan:
return "Assets/Sprites/ObstacleSkins/Skin_Obstacle_StoneLantern.png";
case Platform.PlatformSkin.China:
return "Assets/Sprites/ObstacleSkins/Skin_Obstacle_MarketCrate.png";
case Platform.PlatformSkin.USA:
return "Assets/Sprites/ObstacleSkins/Skin_Obstacle_Cone.png";
case Platform.PlatformSkin.Airport:
default:
return "Assets/Sprites/ObstacleSkins/Skin_Obstacle_Suitcases.png";
}
}
private string GetHangingObstacleSkinPath(Platform.PlatformSkin platformSkin) {
switch(platformSkin)
{
case Platform.PlatformSkin.Japan:
return "Assets/Sprites/ObstacleSkins/Skin_Hanging_RoadSign.png";
case Platform.PlatformSkin.China:
return "Assets/Sprites/ObstacleSkins/Skin_Hanging_RedLanterns.png";
case Platform.PlatformSkin.USA:
return "Assets/Sprites/ObstacleSkins/Skin_Hanging_Banner.png";
case Platform.PlatformSkin.Airport:
default:
return "Assets/Sprites/ObstacleSkins/Skin_Hanging_AirportSign.png";
}
}
private string GetCeilingSkinPath(Platform.PlatformSkin platformSkin) {
if(platformSkin == Platform.PlatformSkin.Airport)
{
return "Assets/Sprites/ObstacleSkins/Skin_Platform_AirportCeiling.png";
}
switch(platformSkin)
{
case Platform.PlatformSkin.Japan:
return "Assets/Sprites/ObstacleSkins/Skin_Platform_Japan.png";
case Platform.PlatformSkin.China:
return "Assets/Sprites/ObstacleSkins/Skin_Platform_China.png";
case Platform.PlatformSkin.USA:
return "Assets/Sprites/ObstacleSkins/Skin_Platform_USA.png";
case Platform.PlatformSkin.Airport:
default:
return "Assets/Sprites/ObstacleSkins/Skin_Platform_AirportFloorTile.png";
}
}
#endif
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a11c0b8fb5a84de9a9ef6ef13f000010
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+283
View File
@@ -0,0 +1,283 @@
using TMPro;
using UnityEngine;
public enum FinishGateStyle {
None,
AirportDeparture,
JapanTorii,
ChinaPaifang,
AmericaRoadSign
}
public class FinishGate : MonoBehaviour {
private const string ForegroundSortingLayer = "Foreground";
private const float DespawnX = -18f;
private const float RuntimeSpritePixelsPerUnit = 256f;
private static Sprite blockSprite;
private bool cleared = false;
public void Setup(FinishGateStyle style) {
cleared = false;
ScrollingObject scrollingObject = GetComponent<ScrollingObject>();
if(scrollingObject == null)
{
scrollingObject = gameObject.AddComponent<ScrollingObject>();
}
scrollingObject.speed = 10f;
BoxCollider2D trigger = GetComponent<BoxCollider2D>();
if(trigger == null)
{
trigger = gameObject.AddComponent<BoxCollider2D>();
}
ConfigureTrigger(trigger);
BuildGate(style);
}
private void Update() {
if(transform.position.x < DespawnX)
{
Destroy(gameObject);
}
}
private void OnTriggerEnter2D(Collider2D other) {
if(cleared || !other.CompareTag("Player") || !GameFlowManager.IsGameplayActive)
{
return;
}
cleared = true;
if(GameManager.instance != null)
{
GameManager.instance.CompleteCurrentStageFromFinishGate();
}
}
private void BuildGate(FinishGateStyle style) {
ClearChildren();
if(TryBuildSpriteGate(style))
{
return;
}
switch(style)
{
case FinishGateStyle.JapanTorii:
BuildJapanTorii();
break;
case FinishGateStyle.ChinaPaifang:
BuildChinaPaifang();
break;
case FinishGateStyle.AmericaRoadSign:
BuildAmericaRoadSign();
break;
case FinishGateStyle.AirportDeparture:
default:
BuildAirportDepartureGate();
break;
}
}
private void ConfigureTrigger(BoxCollider2D trigger) {
trigger.isTrigger = true;
trigger.offset = new Vector2(0f, 1.75f);
trigger.size = new Vector2(2.2f, 4.2f);
}
private bool TryBuildSpriteGate(FinishGateStyle style) {
string resourcePath = GetSpriteResourcePath(style);
if(string.IsNullOrEmpty(resourcePath))
{
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);
}
}
if(sprite == null)
{
return false;
}
GameObject spriteObject = new GameObject("Finish Gate Sprite");
spriteObject.transform.SetParent(transform, false);
SpriteRenderer spriteRenderer = spriteObject.AddComponent<SpriteRenderer>();
spriteRenderer.sprite = sprite;
spriteRenderer.sortingLayerName = ForegroundSortingLayer;
spriteRenderer.sortingOrder = 6;
float targetHeight = GetSpriteGateHeight(style);
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);
return true;
}
private string GetSpriteResourcePath(FinishGateStyle style) {
switch(style)
{
case FinishGateStyle.AirportDeparture:
return "FinishGate_Tutorial_Incheon";
case FinishGateStyle.JapanTorii:
return "FinishGate_Japan_Torii";
case FinishGateStyle.ChinaPaifang:
return "FinishGate_China_Paifang";
case FinishGateStyle.AmericaRoadSign:
return "FinishGate_America_Highway";
default:
return "";
}
}
private float GetSpriteGateHeight(FinishGateStyle style) {
switch(style)
{
case FinishGateStyle.ChinaPaifang:
return 4.65f;
case FinishGateStyle.AmericaRoadSign:
return 4.45f;
default:
return 4.35f;
}
}
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);
Color cyan = new Color(0.18f, 0.83f, 0.95f, 1f);
Color yellow = new Color(1f, 0.78f, 0.22f, 1f);
Color glass = new Color(0.6f, 0.9f, 1f, 0.55f);
CreateBlock("Airport Left Pillar", new Vector2(-1.15f, 1.45f), new Vector2(0.22f, 3.1f), navy, 3);
CreateBlock("Airport Right Pillar", new Vector2(1.15f, 1.45f), new Vector2(0.22f, 3.1f), navy, 3);
CreateBlock("Airport Top Sign", new Vector2(0f, 3.05f), new Vector2(2.75f, 0.64f), blue, 4);
CreateBlock("Airport Sign Highlight", new Vector2(0f, 3.35f), new Vector2(2.75f, 0.08f), cyan, 5);
CreateBlock("Airport Gate Floor", new Vector2(0f, -0.04f), new Vector2(2.9f, 0.12f), yellow, 4);
CreateBlock("Airport Glass Left", new Vector2(-0.62f, 1.35f), new Vector2(0.16f, 2.2f), glass, 2);
CreateBlock("Airport Glass Right", new Vector2(0.62f, 1.35f), new Vector2(0.16f, 2.2f), glass, 2);
CreateBlock("Airport Clear Light", new Vector2(0f, 2.55f), new Vector2(0.26f, 0.14f), yellow, 5);
CreateWorldText("DEPARTURE", new Vector2(0f, 3.07f), 0.34f, Color.white, 6);
}
private void BuildJapanTorii() {
Color red = new Color(0.86f, 0.08f, 0.05f, 1f);
Color darkRed = new Color(0.48f, 0.02f, 0.02f, 1f);
Color black = new Color(0.08f, 0.04f, 0.03f, 1f);
Color gold = new Color(1f, 0.76f, 0.28f, 1f);
CreateBlock("Torii Left Pillar", new Vector2(-1.18f, 1.35f), new Vector2(0.28f, 2.9f), red, 4);
CreateBlock("Torii Right Pillar", new Vector2(1.18f, 1.35f), new Vector2(0.28f, 2.9f), red, 4);
CreateBlock("Torii Left Base", new Vector2(-1.18f, -0.14f), new Vector2(0.48f, 0.22f), black, 3);
CreateBlock("Torii Right Base", new Vector2(1.18f, -0.14f), new Vector2(0.48f, 0.22f), black, 3);
CreateBlock("Torii Lower Beam", new Vector2(0f, 2.55f), new Vector2(2.75f, 0.24f), darkRed, 5);
CreateBlock("Torii Main Beam", new Vector2(0f, 3.05f), new Vector2(3.35f, 0.32f), red, 6);
CreateBlock("Torii Top Cap", new Vector2(0f, 3.31f), new Vector2(3.75f, 0.16f), black, 7);
CreateBlock("Torii Center Plaque", new Vector2(0f, 2.82f), new Vector2(0.36f, 0.5f), gold, 7);
}
private void BuildChinaPaifang() {
Color red = new Color(0.74f, 0.05f, 0.04f, 1f);
Color gold = new Color(1f, 0.72f, 0.18f, 1f);
Color roof = new Color(0.12f, 0.3f, 0.22f, 1f);
Color dark = new Color(0.24f, 0.04f, 0.03f, 1f);
CreateBlock("Paifang Left Pillar", new Vector2(-1.25f, 1.28f), new Vector2(0.28f, 2.8f), red, 4);
CreateBlock("Paifang Right Pillar", new Vector2(1.25f, 1.28f), new Vector2(0.28f, 2.8f), red, 4);
CreateBlock("Paifang Center Pillar Left", new Vector2(-0.42f, 1.48f), new Vector2(0.18f, 2.2f), dark, 3);
CreateBlock("Paifang Center Pillar Right", new Vector2(0.42f, 1.48f), new Vector2(0.18f, 2.2f), dark, 3);
CreateBlock("Paifang Sign", new Vector2(0f, 2.65f), new Vector2(2.45f, 0.46f), gold, 5);
CreateBlock("Paifang Roof", new Vector2(0f, 3.18f), new Vector2(3.25f, 0.26f), roof, 6);
CreateBlock("Paifang Roof Cap", new Vector2(0f, 3.42f), new Vector2(2.35f, 0.2f), roof, 7);
CreateBlock("Paifang Lantern Left", new Vector2(-0.78f, 2.18f), new Vector2(0.26f, 0.34f), red, 7);
CreateBlock("Paifang Lantern Right", new Vector2(0.78f, 2.18f), new Vector2(0.26f, 0.34f), red, 7);
}
private void BuildAmericaRoadSign() {
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);
Color white = Color.white;
CreateBlock("Road Sign Left Pole", new Vector2(-1.3f, 1.35f), new Vector2(0.18f, 3.0f), metal, 3);
CreateBlock("Road Sign Right Pole", new Vector2(1.3f, 1.35f), new Vector2(0.18f, 3.0f), metal, 3);
CreateBlock("Road Sign Panel", new Vector2(0f, 2.65f), new Vector2(3.05f, 0.9f), green, 5);
CreateBlock("Road Sign Stripe", new Vector2(0f, 2.25f), new Vector2(2.75f, 0.08f), white, 6);
CreateBlock("Road Sign Arrow", new Vector2(0f, 2.72f), new Vector2(0.5f, 0.16f), yellow, 7);
CreateWorldText("ARRIVAL", new Vector2(0f, 2.88f), 0.36f, white, 8);
}
private SpriteRenderer CreateBlock(string objectName, Vector2 localPosition, Vector2 size, Color color, int sortingOrder) {
GameObject block = new GameObject(objectName);
block.transform.SetParent(transform, false);
block.transform.localPosition = new Vector3(localPosition.x, localPosition.y, 0f);
block.transform.localScale = new Vector3(size.x, size.y, 1f);
SpriteRenderer renderer = block.AddComponent<SpriteRenderer>();
renderer.sprite = GetBlockSprite();
renderer.color = color;
renderer.sortingLayerName = ForegroundSortingLayer;
renderer.sortingOrder = sortingOrder;
return renderer;
}
private TextMeshPro CreateWorldText(string text, Vector2 localPosition, float fontSize, Color color, int sortingOrder) {
GameObject textObject = new GameObject("Gate Text " + text);
textObject.transform.SetParent(transform, false);
textObject.transform.localPosition = new Vector3(localPosition.x, localPosition.y, -0.05f);
TextMeshPro textMesh = textObject.AddComponent<TextMeshPro>();
textMesh.text = text;
textMesh.fontSize = fontSize;
textMesh.alignment = TextAlignmentOptions.Center;
textMesh.color = color;
textMesh.textWrappingMode = TextWrappingModes.NoWrap;
textMesh.rectTransform.sizeDelta = new Vector2(3f, 0.55f);
textMesh.renderer.sortingLayerName = ForegroundSortingLayer;
textMesh.renderer.sortingOrder = sortingOrder;
return textMesh;
}
private static Sprite GetBlockSprite() {
if(blockSprite != null)
{
return blockSprite;
}
Texture2D texture = new Texture2D(1, 1);
texture.SetPixel(0, 0, Color.white);
texture.Apply();
texture.wrapMode = TextureWrapMode.Clamp;
texture.filterMode = FilterMode.Point;
blockSprite = Sprite.Create(texture, new Rect(0f, 0f, 1f, 1f), new Vector2(0.5f, 0.5f), 1f);
blockSprite.name = "Runtime Finish Gate Block";
return blockSprite;
}
private void ClearChildren() {
for(int i = transform.childCount - 1; i >= 0; i--)
{
Destroy(transform.GetChild(i).gameObject);
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3fa9791b34724eb290a968ddb46f1542
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,58 @@
using UnityEngine;
public class FlowIntroFloatMotion : MonoBehaviour
{
public Vector2 amplitude = Vector2.zero;
public Vector2 frequency = Vector2.one;
public float phase = 0f;
public float scalePulse = 0f;
private RectTransform rectTransform;
private Vector2 basePosition;
private Vector3 baseScale;
private void Awake()
{
rectTransform = GetComponent<RectTransform>();
if(rectTransform != null)
{
basePosition = rectTransform.anchoredPosition;
baseScale = rectTransform.localScale;
}
}
private void OnEnable()
{
if(rectTransform == null)
{
rectTransform = GetComponent<RectTransform>();
}
if(rectTransform != null)
{
basePosition = rectTransform.anchoredPosition;
baseScale = rectTransform.localScale;
}
}
private void Update()
{
if(rectTransform == null)
{
return;
}
float time = Time.unscaledTime + phase;
Vector2 offset = new Vector2(
Mathf.Sin(time * frequency.x) * amplitude.x,
Mathf.Sin(time * frequency.y) * amplitude.y);
rectTransform.anchoredPosition = basePosition + offset;
if(scalePulse > 0f)
{
float scale = 1f + (Mathf.Sin(time * 5.2f) * scalePulse);
rectTransform.localScale = baseScale * scale;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9e6a8a7c6f914625a0c777e6f3fd7101
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+102
View File
@@ -0,0 +1,102 @@
using UnityEngine;
public class FlowIntroRouteMotion : MonoBehaviour
{
public Vector2[] points;
public Vector2 offset;
public float duration = 7.5f;
private RectTransform rectTransform;
private Vector3 baseScale = Vector3.one;
private void Awake()
{
rectTransform = GetComponent<RectTransform>();
if(rectTransform != null)
{
baseScale = rectTransform.localScale;
}
}
private void OnEnable()
{
if(rectTransform == null)
{
rectTransform = GetComponent<RectTransform>();
}
if(rectTransform != null)
{
baseScale = rectTransform.localScale;
}
}
private void Update()
{
if(rectTransform == null || points == null || points.Length < 2 || duration <= 0f)
{
return;
}
float totalLength = GetTotalLength();
if(totalLength <= 0f)
{
return;
}
float distance = Mathf.Repeat(Time.unscaledTime / duration, 1f) * totalLength;
Vector2 position = points[0];
Vector2 direction = Vector2.right;
for(int i = 0; i < points.Length - 1; i++)
{
Vector2 from = points[i];
Vector2 to = points[i + 1];
Vector2 delta = to - from;
float segmentLength = delta.magnitude;
if(distance <= segmentLength)
{
float t = segmentLength > 0f ? distance / segmentLength : 0f;
position = Vector2.Lerp(from, to, t);
direction = delta.normalized;
break;
}
distance -= segmentLength;
}
rectTransform.anchoredPosition = position + offset;
if(direction.sqrMagnitude > 0.0001f)
{
Vector2 displayDirection = direction;
Vector3 displayScale = baseScale;
if(displayDirection.x < 0f)
{
displayDirection.x = -displayDirection.x;
displayScale.x = -Mathf.Abs(baseScale.x);
}
else
{
displayScale.x = Mathf.Abs(baseScale.x);
}
rectTransform.localScale = displayScale;
rectTransform.localRotation = Quaternion.Euler(0f, 0f, Mathf.Atan2(displayDirection.y, displayDirection.x) * Mathf.Rad2Deg - 12f);
}
}
private float GetTotalLength()
{
float totalLength = 0f;
for(int i = 0; i < points.Length - 1; i++)
{
totalLength += Vector2.Distance(points[i], points[i + 1]);
}
return totalLength;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bbe0f2f87b984865b874f7977f0d9588
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,47 @@
using UnityEngine;
public class FlowIntroStampMotion : MonoBehaviour
{
private const float Duration = 0.18f;
private RectTransform rectTransform;
private Vector3 baseScale = Vector3.one;
private float startTime = -1f;
private void Awake()
{
rectTransform = GetComponent<RectTransform>();
if(rectTransform != null)
{
baseScale = rectTransform.localScale;
}
}
public void Play()
{
if(rectTransform == null)
{
rectTransform = GetComponent<RectTransform>();
}
if(rectTransform != null)
{
baseScale = Vector3.one;
rectTransform.localScale = baseScale * 1.42f;
}
startTime = Time.unscaledTime;
}
private void Update()
{
if(rectTransform == null || startTime < 0f)
{
return;
}
float t = Mathf.Clamp01((Time.unscaledTime - startTime) / Duration);
float scale = Mathf.Lerp(1.42f, 1f, 1f - Mathf.Pow(1f - t, 3f));
rectTransform.localScale = baseScale * scale;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c656de70bd9948dca3bc45497f3e073d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+822 -13
View File
@@ -4,10 +4,14 @@ using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
#if UNITY_EDITOR
using UnityEditor;
#endif
public class GameFlowManager : MonoBehaviour {
public enum FlowScreen {
Intro,
OpeningStory,
Game,
Result,
DutyFreeShop,
@@ -39,6 +43,11 @@ public class GameFlowManager : MonoBehaviour {
private const int LunchboxCost = 80;
private const int SpeedShoesCost = 140;
private const int MileageCardCost = 160;
private static readonly string[] OpeningStoryPageAssetPaths = {
"Assets/ConceptArt/Opening/opening_cutscene_comic_jj_03.png",
"Assets/ConceptArt/Opening/opening_cutscene_comic_jj_04_prepare.png",
"Assets/ConceptArt/Opening/opening_cutscene_final_departure_jj_06_street_strict_stylematch.png"
};
private GameSaveData saveData;
private MapDefinition currentMap;
@@ -51,9 +60,12 @@ public class GameFlowManager : MonoBehaviour {
private int lastResultMileage = 0;
private float lastResultTime = 0f;
private float lastResultDistance = 0f;
private int openingStoryPageIndex = 0;
private Canvas overlayCanvas;
private RectTransform overlayRoot;
private Image overlayBackground;
private Camera flowCamera;
private TMP_FontAsset runtimeFont;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
@@ -165,7 +177,8 @@ public class GameFlowManager : MonoBehaviour {
saveData = GameSaveManager.ResetSave();
currentMap = MapDatabase.GetMapByIndex(saveData.currentMapIndex);
completedMap = null;
ShowMapPreview();
openingStoryPageIndex = 0;
ShowOpeningStory();
}
private void ContinueJourney() {
@@ -206,10 +219,7 @@ public class GameFlowManager : MonoBehaviour {
currentScreen = FlowScreen.Game;
gameplayActive = true;
if(overlayCanvas != null)
{
overlayCanvas.gameObject.SetActive(false);
}
SetOverlayVisible(false);
if(GameManager.instance != null)
{
@@ -217,6 +227,23 @@ public class GameFlowManager : MonoBehaviour {
}
}
private void ShowOpeningStory() {
currentScreen = FlowScreen.OpeningStory;
gameplayActive = false;
pendingStartGameplay = false;
if(TryLoadFlowScene(IntroSceneName))
{
return;
}
EnsureOverlay();
ClearOverlay();
SetOverlayVisible(true);
BuildOpeningStoryScreen();
}
private void ShowIntro() {
currentScreen = FlowScreen.Intro;
gameplayActive = false;
@@ -232,12 +259,7 @@ public class GameFlowManager : MonoBehaviour {
ClearOverlay();
SetOverlayVisible(true);
AddTitle("Uni-Run");
AddBody("여권을 챙기고 공항으로 출발합니다.\n진행도는 자동 저장됩니다.");
AddBody("현재 경로: " + currentMap.displayName + "\n해금: " + MapDatabase.GetMapByIndex(saveData.unlockedMapIndex).displayName + "\n마일리지: " + saveData.totalMileage);
AddButton("이어하기", ContinueJourney);
AddButton("새 여행", StartNewJourney);
AddButton("저장 초기화", ResetAndStayIntro);
BuildIntroScreen();
}
private void ShowResult() {
@@ -249,6 +271,7 @@ public class GameFlowManager : MonoBehaviour {
EnsureOverlay();
ClearOverlay();
SetOverlayVisible(true);
SetDefaultOverlayBackground();
string title = lastResultCleared ? "맵 클리어" : "일정 실패";
string body = completedMap.displayName
@@ -290,6 +313,7 @@ public class GameFlowManager : MonoBehaviour {
EnsureOverlay();
ClearOverlay();
SetOverlayVisible(true);
SetDefaultOverlayBackground();
AddTitle("면세점");
AddBody("보유 마일리지: " + saveData.totalMileage
@@ -327,6 +351,7 @@ public class GameFlowManager : MonoBehaviour {
EnsureOverlay();
ClearOverlay();
SetOverlayVisible(true);
SetDefaultOverlayBackground();
AddTitle("다음 경로");
AddBody(currentMap.displayName
@@ -356,6 +381,9 @@ public class GameFlowManager : MonoBehaviour {
case FlowScreen.MapPreview:
ShowMapPreview();
break;
case FlowScreen.OpeningStory:
ShowOpeningStory();
break;
case FlowScreen.Intro:
default:
ShowIntro();
@@ -423,6 +451,7 @@ public class GameFlowManager : MonoBehaviour {
EnsureOverlay();
ClearOverlay();
SetOverlayVisible(true);
SetDefaultOverlayBackground();
AddTitle(title);
AddBody(body);
}
@@ -444,19 +473,21 @@ public class GameFlowManager : MonoBehaviour {
}
private void EnsureOverlay() {
EnsureEventSystem();
if(overlayCanvas != null)
{
return;
}
EnsureEventSystem();
GameObject canvasObject = new GameObject("Flow Overlay", typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster));
DontDestroyOnLoad(canvasObject);
overlayCanvas = canvasObject.GetComponent<Canvas>();
overlayCanvas.renderMode = RenderMode.ScreenSpaceOverlay;
overlayCanvas.worldCamera = null;
overlayCanvas.sortingOrder = 100;
SetUiLayer(canvasObject);
CanvasScaler scaler = canvasObject.GetComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
@@ -465,6 +496,7 @@ public class GameFlowManager : MonoBehaviour {
GameObject rootObject = new GameObject("Flow Overlay Root", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
rootObject.transform.SetParent(canvasObject.transform, false);
SetUiLayer(rootObject);
overlayRoot = rootObject.GetComponent<RectTransform>();
overlayRoot.anchorMin = Vector2.zero;
@@ -473,15 +505,765 @@ public class GameFlowManager : MonoBehaviour {
overlayRoot.offsetMax = Vector2.zero;
Image background = rootObject.GetComponent<Image>();
overlayBackground = background;
background.color = new Color(0.02f, 0.035f, 0.045f, 0.92f);
background.raycastTarget = true;
runtimeFont = CreateKoreanFontAsset();
}
private void SetDefaultOverlayBackground() {
if(overlayBackground != null)
{
overlayBackground.color = new Color(0.02f, 0.035f, 0.045f, 0.92f);
}
if(flowCamera != null)
{
flowCamera.backgroundColor = new Color(0.02f, 0.035f, 0.045f, 1f);
}
}
private void BuildIntroScreen() {
if(overlayBackground != null)
{
overlayBackground.color = Color.clear;
}
if(flowCamera != null)
{
flowCamera.backgroundColor = new Color(0.006f, 0.022f, 0.035f, 1f);
}
RectTransform mapRect = AddIntroWorldMap();
Vector2[] routePoints = GetIntroRoutePoints();
AddIntroRoute(mapRect, routePoints);
AddIntroPlane(mapRect, routePoints);
AddIntroPassportAndPhone();
AddIntroTitlePanel();
AddIntroButtonDock();
}
private void BuildOpeningStoryScreen() {
if(overlayBackground != null)
{
overlayBackground.color = new Color(0.012f, 0.018f, 0.024f, 0.96f);
}
if(flowCamera != null)
{
flowCamera.backgroundColor = new Color(0.012f, 0.018f, 0.024f, 1f);
}
openingStoryPageIndex = Mathf.Clamp(openingStoryPageIndex, 0, OpeningStoryPageAssetPaths.Length - 1);
AddOpeningStoryPage();
AddOpeningStoryProgressDots();
AddOpeningStoryControls();
}
private void AddOpeningStoryPage() {
GameObject shadowObject = CreateUiObject("Opening Story Shadow", overlayRoot, typeof(Image));
RectTransform shadowRect = shadowObject.GetComponent<RectTransform>();
shadowRect.anchorMin = new Vector2(0.5f, 0.5f);
shadowRect.anchorMax = new Vector2(0.5f, 0.5f);
shadowRect.pivot = new Vector2(0.5f, 0.5f);
shadowRect.anchoredPosition = new Vector2(7f, 18f);
shadowRect.sizeDelta = new Vector2(1132f, 637f);
Image shadowImage = shadowObject.GetComponent<Image>();
shadowImage.color = new Color(0f, 0f, 0f, 0.34f);
shadowImage.raycastTarget = false;
GameObject pageObject = CreateUiObject("Opening Story Page", overlayRoot, typeof(Image), typeof(Button));
RectTransform pageRect = pageObject.GetComponent<RectTransform>();
pageRect.anchorMin = new Vector2(0.5f, 0.5f);
pageRect.anchorMax = new Vector2(0.5f, 0.5f);
pageRect.pivot = new Vector2(0.5f, 0.5f);
pageRect.anchoredPosition = new Vector2(0f, 24f);
pageRect.sizeDelta = new Vector2(1120f, 630f);
Image pageImage = pageObject.GetComponent<Image>();
pageImage.sprite = LoadUiTextureSprite(OpeningStoryPageAssetPaths[openingStoryPageIndex]);
pageImage.color = pageImage.sprite != null
? Color.white
: new Color(0.91f, 0.87f, 0.78f, 1f);
pageImage.preserveAspect = true;
Button pageButton = pageObject.GetComponent<Button>();
pageButton.transition = Selectable.Transition.None;
pageButton.onClick.AddListener(AdvanceOpeningStory);
}
private void AddOpeningStoryProgressDots() {
float startX = -((OpeningStoryPageAssetPaths.Length - 1) * 18f) * 0.5f;
for(int i = 0; i < OpeningStoryPageAssetPaths.Length; i++)
{
GameObject dotObject = CreateUiObject("Opening Story Dot " + (i + 1), overlayRoot, typeof(Image));
RectTransform dotRect = dotObject.GetComponent<RectTransform>();
dotRect.anchorMin = new Vector2(0.5f, 0.5f);
dotRect.anchorMax = new Vector2(0.5f, 0.5f);
dotRect.pivot = new Vector2(0.5f, 0.5f);
dotRect.anchoredPosition = new Vector2(startX + (i * 18f), -324f);
dotRect.sizeDelta = i == openingStoryPageIndex ? new Vector2(12f, 12f) : new Vector2(8f, 8f);
Image dotImage = dotObject.GetComponent<Image>();
dotImage.color = i == openingStoryPageIndex
? new Color(1f, 0.86f, 0.28f, 0.95f)
: new Color(0.72f, 0.84f, 0.88f, 0.5f);
dotImage.raycastTarget = false;
}
}
private void AddOpeningStoryControls() {
if(openingStoryPageIndex > 0)
{
AddOpeningStoryButton("이전", new Vector2(-450f, -324f), PreviousOpeningStoryPage);
}
string nextLabel = openingStoryPageIndex >= OpeningStoryPageAssetPaths.Length - 1 ? "게임 시작" : "다음";
AddOpeningStoryButton(nextLabel, new Vector2(450f, -324f), AdvanceOpeningStory);
}
private void AddOpeningStoryButton(string label, Vector2 position, UnityEngine.Events.UnityAction action) {
GameObject buttonObject = CreateUiObject("Opening Story Button " + label, overlayRoot, typeof(Image), typeof(Button));
RectTransform buttonRect = buttonObject.GetComponent<RectTransform>();
buttonRect.anchorMin = new Vector2(0.5f, 0.5f);
buttonRect.anchorMax = new Vector2(0.5f, 0.5f);
buttonRect.pivot = new Vector2(0.5f, 0.5f);
buttonRect.anchoredPosition = position;
buttonRect.sizeDelta = label == "게임 시작" ? new Vector2(178f, 44f) : new Vector2(126f, 42f);
Image buttonImage = buttonObject.GetComponent<Image>();
buttonImage.color = label == "게임 시작"
? new Color(1f, 0.86f, 0.34f, 0.96f)
: new Color(0.88f, 0.96f, 1f, 0.88f);
Button button = buttonObject.GetComponent<Button>();
button.onClick.AddListener(action);
TextMeshProUGUI buttonText = AddTextTo(buttonObject.transform, "Label", label, label == "게임 시작" ? 17f : 15f, FontStyles.Bold);
RectTransform textRect = buttonText.rectTransform;
textRect.anchorMin = Vector2.zero;
textRect.anchorMax = Vector2.one;
textRect.offsetMin = Vector2.zero;
textRect.offsetMax = Vector2.zero;
buttonText.color = new Color(0.035f, 0.065f, 0.08f, 1f);
}
private void AdvanceOpeningStory() {
if(openingStoryPageIndex >= OpeningStoryPageAssetPaths.Length - 1)
{
LoadGameSceneForCurrentMap();
return;
}
openingStoryPageIndex++;
ShowOpeningStory();
}
private void PreviousOpeningStoryPage() {
if(openingStoryPageIndex <= 0)
{
return;
}
openingStoryPageIndex--;
ShowOpeningStory();
}
private RectTransform AddIntroWorldMap() {
GameObject mapObject = CreateUiObject("Intro World Map", overlayRoot, typeof(Image));
RectTransform mapRect = mapObject.GetComponent<RectTransform>();
mapRect.anchorMin = new Vector2(0.5f, 0.5f);
mapRect.anchorMax = new Vector2(0.5f, 0.5f);
mapRect.pivot = new Vector2(0.5f, 0.5f);
mapRect.anchoredPosition = Vector2.zero;
mapRect.sizeDelta = new Vector2(1280f, 720f);
Image mapImage = mapObject.GetComponent<Image>();
mapImage.sprite = LoadUiSprite("", "Assets/ConceptArt/Intro/intro_world_map_clean_01.png");
mapImage.color = mapImage.sprite != null
? Color.white
: new Color(0.015f, 0.078f, 0.112f, 0.76f);
mapImage.preserveAspect = true;
mapImage.raycastTarget = false;
GameObject vignetteObject = CreateUiObject("Intro Map Vignette", overlayRoot, typeof(Image));
RectTransform vignetteRect = vignetteObject.GetComponent<RectTransform>();
vignetteRect.anchorMin = Vector2.zero;
vignetteRect.anchorMax = Vector2.one;
vignetteRect.offsetMin = Vector2.zero;
vignetteRect.offsetMax = Vector2.zero;
Image vignetteImage = vignetteObject.GetComponent<Image>();
vignetteImage.color = new Color(0f, 0.006f, 0.012f, 0.08f);
vignetteImage.raycastTarget = false;
return mapRect;
}
private void AddIntroMapGrid(RectTransform mapRect) {
Color gridColor = new Color(0.46f, 0.86f, 1f, 0.12f);
for(int i = -5; i <= 5; i++)
{
GameObject lineObject = CreateUiObject("Intro Map Longitude " + i, mapRect, typeof(Image));
RectTransform lineRect = lineObject.GetComponent<RectTransform>();
lineRect.anchorMin = new Vector2(0.5f, 0.5f);
lineRect.anchorMax = new Vector2(0.5f, 0.5f);
lineRect.pivot = new Vector2(0.5f, 0.5f);
lineRect.anchoredPosition = new Vector2(i * 92f, 0f);
lineRect.sizeDelta = new Vector2(2f, 610f);
Image lineImage = lineObject.GetComponent<Image>();
lineImage.color = gridColor;
lineImage.raycastTarget = false;
}
for(int i = -3; i <= 3; i++)
{
GameObject lineObject = CreateUiObject("Intro Map Latitude " + i, mapRect, typeof(Image));
RectTransform lineRect = lineObject.GetComponent<RectTransform>();
lineRect.anchorMin = new Vector2(0.5f, 0.5f);
lineRect.anchorMax = new Vector2(0.5f, 0.5f);
lineRect.pivot = new Vector2(0.5f, 0.5f);
lineRect.anchoredPosition = new Vector2(0f, i * 74f);
lineRect.sizeDelta = new Vector2(1110f, 2f);
Image lineImage = lineObject.GetComponent<Image>();
lineImage.color = gridColor;
lineImage.raycastTarget = false;
}
}
private void AddIntroLandMass(RectTransform mapRect, string objectName, Vector2 position, Vector2 size, float rotation) {
GameObject landObject = CreateUiObject(objectName, mapRect, typeof(Image));
RectTransform landRect = landObject.GetComponent<RectTransform>();
landRect.anchorMin = new Vector2(0.5f, 0.5f);
landRect.anchorMax = new Vector2(0.5f, 0.5f);
landRect.pivot = new Vector2(0.5f, 0.5f);
landRect.anchoredPosition = position;
landRect.sizeDelta = size;
landRect.localRotation = Quaternion.Euler(0f, 0f, rotation);
Image landImage = landObject.GetComponent<Image>();
landImage.color = new Color(0.24f, 0.72f, 0.59f, 0.22f);
landImage.raycastTarget = false;
}
private Vector2[] GetIntroRoutePoints() {
return new Vector2[] {
new Vector2(356f, 50f),
new Vector2(405f, 58f),
new Vector2(316f, 42f),
new Vector2(-450f, 94f)
};
}
private void AddIntroRoute(RectTransform mapRect, Vector2[] routePoints) {
Color routeColor = new Color(1f, 0.86f, 0.28f, 0.82f);
for(int i = 0; i < routePoints.Length - 1; i++)
{
AddIntroRouteSegment(mapRect, routePoints[i], routePoints[i + 1], routeColor, 4f);
}
AddIntroRouteMarker(mapRect, routePoints[0], "INCHEON", true, 0f, new Vector2(-4f, -25f));
AddIntroRouteMarker(mapRect, routePoints[1], "JAPAN", false, 0.4f, new Vector2(28f, -20f));
AddIntroRouteMarker(mapRect, routePoints[2], "CHINA", false, 0.8f, new Vector2(-18f, -24f));
AddIntroRouteMarker(mapRect, routePoints[3], "AMERICA", false, 1.2f, new Vector2(0f, -25f));
}
private void AddIntroRouteSegment(RectTransform mapRect, Vector2 from, Vector2 to, Color color, float thickness) {
GameObject segmentObject = CreateUiObject("Intro Route Segment", mapRect, typeof(Image));
RectTransform segmentRect = segmentObject.GetComponent<RectTransform>();
Vector2 delta = to - from;
segmentRect.anchorMin = new Vector2(0.5f, 0.5f);
segmentRect.anchorMax = new Vector2(0.5f, 0.5f);
segmentRect.pivot = new Vector2(0.5f, 0.5f);
segmentRect.anchoredPosition = from + (delta * 0.5f);
segmentRect.sizeDelta = new Vector2(delta.magnitude, thickness);
segmentRect.localRotation = Quaternion.Euler(0f, 0f, Mathf.Atan2(delta.y, delta.x) * Mathf.Rad2Deg);
Image segmentImage = segmentObject.GetComponent<Image>();
segmentImage.color = color;
segmentImage.raycastTarget = false;
}
private void AddIntroRouteMarker(RectTransform mapRect, Vector2 position, string label, bool current, float phase, Vector2 labelOffset) {
GameObject markerObject = CreateUiObject("Intro Route Marker " + label, mapRect, typeof(Image));
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 = position;
markerRect.sizeDelta = current ? new Vector2(23f, 23f) : new Vector2(17f, 17f);
Image markerImage = markerObject.GetComponent<Image>();
markerImage.color = current
? new Color(1f, 0.92f, 0.22f, 0.95f)
: new Color(0.78f, 0.96f, 1f, 0.86f);
markerImage.raycastTarget = false;
FlowIntroFloatMotion markerMotion = markerObject.AddComponent<FlowIntroFloatMotion>();
markerMotion.scalePulse = current ? 0.18f : 0.09f;
markerMotion.frequency = new Vector2(0f, 0.72f);
markerMotion.phase = phase;
TextMeshProUGUI labelText = AddIntroMapLabel(mapRect, "Intro Marker Label " + label, label, position + labelOffset, new Vector2(84f, 20f), 9.5f);
labelText.color = current
? new Color(1f, 0.94f, 0.44f, 0.92f)
: new Color(0.78f, 0.94f, 1f, 0.72f);
}
private TextMeshProUGUI AddIntroMapLabel(RectTransform parent, string objectName, string text, Vector2 position, Vector2 size, float fontSize) {
GameObject textObject = CreateUiObject(objectName, parent);
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 = position;
textRect.sizeDelta = size;
TextMeshProUGUI textComponent = textObject.AddComponent<TextMeshProUGUI>();
textComponent.text = text;
textComponent.font = runtimeFont != null ? runtimeFont : textComponent.font;
textComponent.fontSize = fontSize;
textComponent.fontStyle = FontStyles.Bold;
textComponent.alignment = TextAlignmentOptions.Center;
textComponent.raycastTarget = false;
textComponent.textWrappingMode = TextWrappingModes.NoWrap;
return textComponent;
}
private void AddIntroPlane(RectTransform mapRect, Vector2[] routePoints) {
Sprite planeSprite = LoadUiSprite("", "Assets/Sprites/UIIcons/UI_AirplaneTravel.png");
GameObject planeObject = CreateUiObject("Intro Airplane", overlayRoot, typeof(Image));
RectTransform rectTransform = planeObject.GetComponent<RectTransform>();
rectTransform.anchorMin = new Vector2(0.5f, 0.5f);
rectTransform.anchorMax = new Vector2(0.5f, 0.5f);
rectTransform.pivot = new Vector2(0.5f, 0.5f);
rectTransform.anchoredPosition = routePoints[0] + mapRect.anchoredPosition;
rectTransform.sizeDelta = new Vector2(58f, 58f);
Image image = planeObject.GetComponent<Image>();
image.sprite = planeSprite;
image.color = planeSprite != null
? new Color(1f, 1f, 1f, 0.95f)
: new Color(0.9f, 0.97f, 1f, 0.88f);
image.preserveAspect = true;
image.raycastTarget = false;
FlowIntroRouteMotion motion = planeObject.AddComponent<FlowIntroRouteMotion>();
motion.points = routePoints;
motion.offset = mapRect.anchoredPosition;
motion.duration = 7.5f;
}
private void AddIntroPassportAndPhone() {
AddIntroPassportCover();
AddIntroSmartPhone();
}
private void AddIntroPassportCover() {
GameObject passportObject = CreateUiObject("Intro Passport Cover", overlayRoot, typeof(Image));
RectTransform passportRect = passportObject.GetComponent<RectTransform>();
passportRect.anchorMin = new Vector2(0.5f, 0.5f);
passportRect.anchorMax = new Vector2(0.5f, 0.5f);
passportRect.pivot = new Vector2(0.5f, 0.5f);
passportRect.anchoredPosition = new Vector2(424f, -242f);
passportRect.sizeDelta = new Vector2(270f, 300f);
passportRect.localRotation = Quaternion.Euler(0f, 0f, -24f);
Image passportImage = passportObject.GetComponent<Image>();
passportImage.sprite = LoadUiTextureSprite("Assets/ConceptArt/Intro/UI/intro_passport_clean.png");
passportImage.color = passportImage.sprite != null
? new Color(1f, 1f, 1f, 0.98f)
: new Color(0.034f, 0.18f, 0.22f, 0.96f);
passportImage.preserveAspect = true;
passportImage.raycastTarget = false;
}
private void AddIntroSmartPhone() {
GameObject phoneObject = CreateUiObject("Intro Smart Phone", overlayRoot, typeof(Image));
RectTransform phoneRect = phoneObject.GetComponent<RectTransform>();
phoneRect.anchorMin = new Vector2(0.5f, 0.5f);
phoneRect.anchorMax = new Vector2(0.5f, 0.5f);
phoneRect.pivot = new Vector2(0.5f, 0.5f);
phoneRect.anchoredPosition = new Vector2(558f, -218f);
phoneRect.sizeDelta = new Vector2(132f, 252f);
phoneRect.localRotation = Quaternion.Euler(0f, 0f, 10f);
Image phoneImage = phoneObject.GetComponent<Image>();
phoneImage.sprite = LoadUiTextureSprite("Assets/ConceptArt/Intro/UI/intro_phone_clean.png");
phoneImage.color = phoneImage.sprite != null
? new Color(1f, 1f, 1f, 0.98f)
: new Color(0.015f, 0.018f, 0.022f, 0.98f);
phoneImage.preserveAspect = true;
phoneImage.raycastTarget = false;
FlowIntroFloatMotion phoneMotion = phoneObject.AddComponent<FlowIntroFloatMotion>();
phoneMotion.amplitude = new Vector2(0f, 5f);
phoneMotion.frequency = new Vector2(0f, 0.45f);
phoneMotion.phase = 0.6f;
}
private void AddIntroTitlePanel() {
GameObject titleObject = CreateUiObject("Intro Title Lockup", overlayRoot, typeof(Image));
RectTransform titleRect = titleObject.GetComponent<RectTransform>();
titleRect.anchorMin = new Vector2(0.5f, 0.5f);
titleRect.anchorMax = new Vector2(0.5f, 0.5f);
titleRect.pivot = new Vector2(0.5f, 0.5f);
titleRect.anchoredPosition = new Vector2(-364f, 196f);
titleRect.sizeDelta = new Vector2(448f, 166f);
Image titleImage = titleObject.GetComponent<Image>();
titleImage.sprite = LoadUiTextureSprite("Assets/ConceptArt/Intro/UI/intro_title_lockup.png");
titleImage.color = titleImage.sprite != null
? Color.white
: new Color(0.86f, 0.98f, 1f, 0.94f);
titleImage.preserveAspect = true;
titleImage.raycastTarget = false;
}
private void AddIntroButtonDock() {
GameObject dockObject = CreateUiObject("Intro Button Dock", overlayRoot);
RectTransform dockRect = dockObject.GetComponent<RectTransform>();
dockRect.anchorMin = new Vector2(0.5f, 0.5f);
dockRect.anchorMax = new Vector2(0.5f, 0.5f);
dockRect.pivot = new Vector2(0.5f, 0.5f);
dockRect.anchoredPosition = new Vector2(0f, -224f);
dockRect.sizeDelta = new Vector2(424f, 136f);
AddIntroButton(dockRect, "게임 시작", new Vector2(45f, 0f), StartNewJourney, true, 0);
AddIntroButton(dockRect, "이어하기", new Vector2(0f, -76f), ContinueJourney, false, 1);
AddIntroButton(dockRect, "설정", new Vector2(220f, -76f), ShowIntroSettingsPlaceholder, false, 2);
}
private void AddIntroButton(RectTransform parent, string label, Vector2 position, UnityEngine.Events.UnityAction action, bool primary, int buttonStyle) {
GameObject buttonObject = CreateUiObject("Button " + label, parent, typeof(Image), typeof(Button));
RectTransform buttonRect = buttonObject.GetComponent<RectTransform>();
buttonRect.anchorMin = new Vector2(0f, 1f);
buttonRect.anchorMax = new Vector2(0f, 1f);
buttonRect.pivot = new Vector2(0f, 1f);
buttonRect.anchoredPosition = position;
buttonRect.sizeDelta = primary ? new Vector2(334f, 74f) : new Vector2(204f, 58f);
Image buttonImage = buttonObject.GetComponent<Image>();
buttonImage.sprite = GetIntroButtonSprite(primary, buttonStyle);
buttonImage.color = buttonImage.sprite != null
? Color.white
: primary
? new Color(0.98f, 0.88f, 0.38f, 0.96f)
: new Color(0.88f, 0.96f, 1f, 0.9f);
buttonImage.preserveAspect = true;
buttonImage.raycastTarget = true;
Button button = buttonObject.GetComponent<Button>();
button.targetGraphic = buttonImage;
button.navigation = new Navigation {
mode = Navigation.Mode.None
};
GameObject stampObject = CreateUiObject("Departure Stamp", buttonObject.transform, typeof(Image));
RectTransform stampRect = stampObject.GetComponent<RectTransform>();
stampRect.anchorMin = new Vector2(1f, 0.5f);
stampRect.anchorMax = new Vector2(1f, 0.5f);
stampRect.pivot = new Vector2(0.5f, 0.5f);
stampRect.anchoredPosition = new Vector2(primary ? -47f : -28f, primary ? 1f : 0f);
stampRect.sizeDelta = primary ? new Vector2(58f, 58f) : new Vector2(44f, 44f);
stampRect.localRotation = Quaternion.Euler(0f, 0f, -12f);
Image stampImage = stampObject.GetComponent<Image>();
stampImage.sprite = LoadUiSpriteRegion("Assets/ConceptArt/Intro/intro_stamps_01.png", new Rect(54f, 48f, 790f, 790f));
stampImage.color = stampImage.sprite != null
? new Color(1f, 1f, 1f, 0.94f)
: new Color(0.76f, 0.08f, 0.06f, 0.96f);
stampImage.preserveAspect = true;
stampImage.raycastTarget = false;
TextMeshProUGUI stampText = AddTextTo(stampObject.transform, "Stamp Label", "출국", primary ? 13f : 11f, FontStyles.Bold);
RectTransform stampTextRect = stampText.rectTransform;
stampTextRect.anchorMin = Vector2.zero;
stampTextRect.anchorMax = Vector2.one;
stampTextRect.offsetMin = Vector2.zero;
stampTextRect.offsetMax = Vector2.zero;
stampText.color = new Color(0.74f, 0.04f, 0.03f, 0.98f);
stampObject.SetActive(false);
button.onClick.AddListener(delegate {
button.interactable = false;
stampObject.SetActive(true);
FlowIntroStampMotion stampMotion = stampObject.GetComponent<FlowIntroStampMotion>();
if(stampMotion == null)
{
stampMotion = stampObject.AddComponent<FlowIntroStampMotion>();
}
stampMotion.Play();
StartCoroutine(InvokeIntroActionAfterStamp(action));
});
if(primary)
{
FlowIntroFloatMotion motion = buttonObject.AddComponent<FlowIntroFloatMotion>();
motion.scalePulse = 0.018f;
motion.frequency = new Vector2(0f, 0.55f);
}
}
private void AddIntroButtonIcon(Transform parent, int buttonStyle) {
GameObject iconObject = CreateUiObject("Button Icon", parent, typeof(Image));
RectTransform iconRect = iconObject.GetComponent<RectTransform>();
iconRect.anchorMin = new Vector2(0.5f, 0.5f);
iconRect.anchorMax = new Vector2(0.5f, 0.5f);
iconRect.pivot = new Vector2(0.5f, 0.5f);
iconRect.anchoredPosition = new Vector2(-38f, 0f);
iconRect.sizeDelta = new Vector2(30f, 30f);
Image iconImage = iconObject.GetComponent<Image>();
iconImage.sprite = GetIntroButtonIconSprite(buttonStyle);
iconImage.color = iconImage.sprite != null
? new Color(1f, 1f, 1f, 0.96f)
: new Color(0.94f, 0.98f, 1f, 0.92f);
iconImage.preserveAspect = true;
iconImage.raycastTarget = false;
}
private Sprite GetIntroButtonSprite(bool primary, int buttonStyle) {
if(primary)
{
return LoadUiTextureSprite("Assets/ConceptArt/Intro/UI/intro_button_start.png");
}
if(buttonStyle == 2)
{
return LoadUiTextureSprite("Assets/ConceptArt/Intro/UI/intro_button_settings.png");
}
return LoadUiTextureSprite("Assets/ConceptArt/Intro/UI/intro_button_continue.png");
}
private Sprite GetIntroButtonIconSprite(int buttonStyle) {
if(buttonStyle == 2)
{
return LoadUiTextureSprite("Assets/ConceptArt/Intro/Cutouts/intro_icon_settings.png");
}
return LoadUiTextureSprite("Assets/ConceptArt/Intro/Cutouts/intro_icon_continue.png");
}
private void ShowIntroSettingsPlaceholder() {
ShowIntro();
}
private IEnumerator InvokeIntroActionAfterStamp(UnityEngine.Events.UnityAction action) {
yield return new WaitForSecondsRealtime(0.32f);
if(action != null)
{
action.Invoke();
}
}
private TextMeshProUGUI AddTextToPanel(RectTransform parent, string objectName, string text, Vector2 position, Vector2 size, float fontSize, FontStyles fontStyle, TextAlignmentOptions alignment) {
GameObject textObject = CreateUiObject(objectName, parent);
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 = position;
textRect.sizeDelta = size;
TextMeshProUGUI textComponent = textObject.AddComponent<TextMeshProUGUI>();
textComponent.text = text;
textComponent.font = runtimeFont != null ? runtimeFont : textComponent.font;
textComponent.fontSize = fontSize;
textComponent.fontStyle = fontStyle;
textComponent.alignment = alignment;
textComponent.color = new Color(0.95f, 0.98f, 1f, 0.96f);
textComponent.raycastTarget = false;
textComponent.textWrappingMode = TextWrappingModes.Normal;
return textComponent;
}
private GameObject CreateUiObject(string objectName, Transform parent, params System.Type[] components) {
System.Type[] finalComponents;
if(components == null || components.Length == 0)
{
finalComponents = new System.Type[] {
typeof(RectTransform),
typeof(CanvasRenderer)
};
}
else
{
finalComponents = new System.Type[components.Length + 2];
finalComponents[0] = typeof(RectTransform);
finalComponents[1] = typeof(CanvasRenderer);
for(int i = 0; i < components.Length; i++)
{
finalComponents[i + 2] = components[i];
}
}
GameObject gameObject = new GameObject(objectName, finalComponents);
gameObject.transform.SetParent(parent, false);
SetUiLayer(gameObject);
return gameObject;
}
private void SetUiLayer(GameObject gameObject) {
if(gameObject != null)
{
gameObject.layer = GetUiLayer();
}
}
private int GetUiLayer() {
int uiLayer = LayerMask.NameToLayer("UI");
return uiLayer >= 0 ? uiLayer : 0;
}
private Sprite LoadUiSprite(string resourcePath, string assetPath) {
if(!string.IsNullOrEmpty(resourcePath))
{
Sprite resourceSprite = Resources.Load<Sprite>(resourcePath);
if(resourceSprite != null)
{
return resourceSprite;
}
}
#if UNITY_EDITOR
if(!string.IsNullOrEmpty(assetPath))
{
Object[] assets = AssetDatabase.LoadAllAssetRepresentationsAtPath(assetPath);
if(assets != null)
{
for(int i = 0; i < assets.Length; i++)
{
Sprite sprite = assets[i] as Sprite;
if(sprite != null)
{
return sprite;
}
}
}
Sprite directSprite = AssetDatabase.LoadAssetAtPath<Sprite>(assetPath);
if(directSprite != null)
{
return directSprite;
}
Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>(assetPath);
if(texture == null)
{
AssetDatabase.Refresh();
texture = AssetDatabase.LoadAssetAtPath<Texture2D>(assetPath);
}
if(texture != null)
{
return Sprite.Create(texture, new Rect(0f, 0f, texture.width, texture.height), new Vector2(0.5f, 0.5f), 100f);
}
}
#endif
return null;
}
private Sprite LoadUiTextureSprite(string assetPath) {
#if UNITY_EDITOR
if(string.IsNullOrEmpty(assetPath))
{
return null;
}
Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>(assetPath);
if(texture == null)
{
AssetDatabase.Refresh();
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 Sprite LoadUiSpriteRegion(string assetPath, Rect pixelRect) {
#if UNITY_EDITOR
if(string.IsNullOrEmpty(assetPath))
{
return null;
}
Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>(assetPath);
if(texture == null)
{
AssetDatabase.Refresh();
texture = AssetDatabase.LoadAssetAtPath<Texture2D>(assetPath);
}
if(texture == null)
{
return null;
}
Rect safeRect = ClampSpriteRect(pixelRect, texture.width, texture.height);
if(safeRect.width <= 0f || safeRect.height <= 0f)
{
return null;
}
return Sprite.Create(texture, safeRect, new Vector2(0.5f, 0.5f), 100f);
#else
return null;
#endif
}
private Rect ClampSpriteRect(Rect pixelRect, int textureWidth, int textureHeight) {
float xMin = Mathf.Clamp(pixelRect.xMin, 0f, textureWidth);
float yMin = Mathf.Clamp(pixelRect.yMin, 0f, textureHeight);
float xMax = Mathf.Clamp(pixelRect.xMax, 0f, textureWidth);
float yMax = Mathf.Clamp(pixelRect.yMax, 0f, textureHeight);
if(xMax < xMin)
{
xMax = xMin;
}
if(yMax < yMin)
{
yMax = yMin;
}
return Rect.MinMaxRect(xMin, yMin, xMax, yMax);
}
private void EnsureEventSystem() {
if(EventSystem.current != null)
{
EventSystem.current.enabled = true;
EventSystem.current.gameObject.SetActive(true);
StandaloneInputModule inputModule = EventSystem.current.GetComponent<StandaloneInputModule>();
if(inputModule == null)
{
inputModule = EventSystem.current.gameObject.AddComponent<StandaloneInputModule>();
}
inputModule.enabled = true;
DontDestroyOnLoad(EventSystem.current.gameObject);
return;
}
@@ -489,6 +1271,25 @@ public class GameFlowManager : MonoBehaviour {
DontDestroyOnLoad(eventSystemObject);
}
private void EnsureFlowCamera() {
if(flowCamera != null)
{
return;
}
GameObject cameraObject = new GameObject("Flow UI Camera", typeof(Camera));
DontDestroyOnLoad(cameraObject);
flowCamera = cameraObject.GetComponent<Camera>();
flowCamera.transform.position = new Vector3(640f, 360f, -10f);
flowCamera.clearFlags = CameraClearFlags.SolidColor;
flowCamera.backgroundColor = new Color(0.006f, 0.022f, 0.035f, 1f);
flowCamera.cullingMask = 1 << GetUiLayer();
flowCamera.depth = 100f;
flowCamera.orthographic = true;
flowCamera.orthographicSize = 360f;
}
private void ClearOverlay() {
if(overlayRoot == null)
{
@@ -506,6 +1307,11 @@ public class GameFlowManager : MonoBehaviour {
{
overlayCanvas.gameObject.SetActive(visible);
}
if(flowCamera != null)
{
flowCamera.gameObject.SetActive(visible);
}
}
private void AddTitle(string text) {
@@ -527,6 +1333,7 @@ public class GameFlowManager : MonoBehaviour {
GameObject buttonObject = new GameObject("Button " + label, typeof(RectTransform), typeof(CanvasRenderer), typeof(Image), typeof(Button));
buttonObject.transform.SetParent(overlayRoot, false);
SetUiLayer(buttonObject);
RectTransform buttonRect = buttonObject.GetComponent<RectTransform>();
buttonRect.anchorMin = new Vector2(0.5f, 0.5f);
@@ -581,6 +1388,7 @@ public class GameFlowManager : MonoBehaviour {
private TextMeshProUGUI AddText(string objectName, string text, float fontSize, FontStyles fontStyle) {
GameObject textObject = new GameObject(objectName + " " + GetBodyCount(), typeof(RectTransform), typeof(CanvasRenderer));
textObject.transform.SetParent(overlayRoot, false);
SetUiLayer(textObject);
RectTransform rectTransform = textObject.GetComponent<RectTransform>();
rectTransform.anchorMin = new Vector2(0.5f, 0.5f);
@@ -602,6 +1410,7 @@ public class GameFlowManager : MonoBehaviour {
private TextMeshProUGUI AddTextTo(Transform parent, string objectName, string text, float fontSize, FontStyles fontStyle) {
GameObject textObject = new GameObject(objectName, typeof(RectTransform), typeof(CanvasRenderer));
textObject.transform.SetParent(parent, false);
SetUiLayer(textObject);
TextMeshProUGUI label = textObject.AddComponent<TextMeshProUGUI>();
label.text = text;
label.font = runtimeFont != null ? runtimeFont : label.font;
+690 -22
View File
@@ -1,3 +1,5 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using TMPro;
@@ -40,7 +42,7 @@ public class GameManager : MonoBehaviour {
private string tutorialTitle = ""; // 튜토리얼 안내 제목
private string tutorialMessage = ""; // 튜토리얼 안내 문구
private Image[] lifeIcons; // 왼쪽 HUD의 목숨 도장 표시
private GameObject tutorialGuideObject; // 화면 단 이미지 튜토리얼 패널
private GameObject tutorialGuideObject; // 화면 단 이미지 튜토리얼 패널
private Image tutorialMainImage; // 점프/슬라이드 대표 이미지
private TextMeshProUGUI tutorialMultiplierText; // 2단 점프 x2 표시
private TextMeshProUGUI tutorialTitleText; // 튜토리얼 제목
@@ -54,16 +56,28 @@ public class GameManager : MonoBehaviour {
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 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";
@@ -86,6 +100,10 @@ public class GameManager : MonoBehaviour {
private float itemInvincibleEndTime = 0f; // 라멘 무적 종료 시각
private float mileageBonusEndTime = 0f; // 마일리지 카드 보너스 종료 시각
public bool IsTutorialGuidePaused {
get { return tutorialGuidePauseActive; }
}
// 게임 시작과 동시에 싱글톤을 구성
void Awake() {
// 싱글톤 변수 instance가 비어있는가?
@@ -120,7 +138,6 @@ public class GameManager : MonoBehaviour {
}
BuildRuntimeHUD();
StartTutorialGuideSequence();
BeginMap(GameFlowManager.Instance != null
? GameFlowManager.Instance.CurrentMap
: MapDatabase.GetMap(MapId.TutorialIncheon));
@@ -153,11 +170,15 @@ public class GameManager : MonoBehaviour {
gameSpeed = Mathf.Min(gameSpeed, maxGameSpeed);
stageDistance += 10f * gameSpeed * Time.deltaTime;
UpdateFinishGate();
if(currentMapDefinition != null && currentMapDefinition.targetDistance > 0f && stageDistance >= currentMapDefinition.targetDistance)
{
CompleteStage(true);
return;
if(!ShouldUseFinishGate() || stageDistance >= currentMapDefinition.targetDistance + finishGateFallbackDistance)
{
CompleteStage(true);
return;
}
}
if(currentMapDefinition != null && currentMapDefinition.timeLimit > 0f && stageElapsedTime >= currentMapDefinition.timeLimit)
@@ -171,9 +192,13 @@ public class GameManager : MonoBehaviour {
}
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;
@@ -185,10 +210,12 @@ public class GameManager : MonoBehaviour {
stageElapsedTime = 0f;
stageDistance = 0f;
stageCompleted = false;
finishGateSpawned = false;
isGameover = false;
tutorialGuideSequenceComplete = false;
currentTutorialGuideSequenceIndex = -1;
SetTutorialGuide(TutorialGuideType.None, "", "");
RemoveActiveFinishGate();
if(gameoverUI != null)
{
@@ -201,6 +228,7 @@ public class GameManager : MonoBehaviour {
platformSpawner.ApplyMap(currentMapDefinition);
}
StartTutorialGuideSequence();
UpdateScoreUI();
}
@@ -220,15 +248,99 @@ public class GameManager : MonoBehaviour {
}
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(!playTutorialGuideSequence)
if(!ShouldUseTimeBasedTutorialGuide())
{
tutorialGuideSequenceComplete = true;
return;
}
@@ -239,7 +351,7 @@ public class GameManager : MonoBehaviour {
}
private void UpdateTutorialGuideSequence() {
if(!playTutorialGuideSequence || tutorialGuideSequenceComplete)
if(!ShouldUseTimeBasedTutorialGuide() || tutorialGuideSequenceComplete)
{
return;
}
@@ -256,6 +368,33 @@ public class GameManager : MonoBehaviour {
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)
{
@@ -359,23 +498,38 @@ public class GameManager : MonoBehaviour {
case TutorialItemType.HealthSushi:
currentLife = Mathf.Min(maxLife, currentLife + 1);
AddScore(3);
SetTutorialGuide(TutorialGuideType.Items, "초밥", "방금 잃은 목숨을 1 회복합니다.");
if(ShouldShowItemEffectGuide())
{
SetTutorialGuide(TutorialGuideType.Items, "초밥", "방금 잃은 목숨을 1 회복합니다.");
}
break;
case TutorialItemType.InvincibleRamen:
itemInvincibleEndTime = Time.time + itemInvincibleDuration;
SetTutorialGuide(TutorialGuideType.Items, "라멘", "짧은 시간 동안 다음 충돌 피해를 받지 않습니다.");
if(ShouldShowItemEffectGuide())
{
SetTutorialGuide(TutorialGuideType.Items, "라멘", "짧은 시간 동안 다음 충돌 피해를 받지 않습니다.");
}
break;
case TutorialItemType.Shield:
shieldCharges = Mathf.Max(shieldCharges, 1);
SetTutorialGuide(TutorialGuideType.Items, "방어막", "다음 충돌 1회를 방어막이 대신 막습니다.");
if(ShouldShowItemEffectGuide())
{
SetTutorialGuide(TutorialGuideType.Items, "방어막", "다음 충돌 1회를 방어막이 대신 막습니다.");
}
break;
case TutorialItemType.SpeedShoes:
gameSpeed = Mathf.Min(maxGameSpeed, gameSpeed + itemSpeedBoostAmount);
SetTutorialGuide(TutorialGuideType.Items, "운동화", "속도를 즉시 회복해 늦어진 일정을 따라잡습니다.");
if(ShouldShowItemEffectGuide())
{
SetTutorialGuide(TutorialGuideType.Items, "운동화", "속도를 즉시 회복해 늦어진 일정을 따라잡습니다.");
}
break;
case TutorialItemType.MileageCard:
mileageBonusEndTime = Time.time + mileageBonusDuration;
SetTutorialGuide(TutorialGuideType.Items, "마일리지 카드", "잠시 동안 먹는 마일리지가 두 배로 들어옵니다.");
if(ShouldShowItemEffectGuide())
{
SetTutorialGuide(TutorialGuideType.Items, "마일리지 카드", "잠시 동안 먹는 마일리지가 두 배로 들어옵니다.");
}
break;
}
@@ -390,14 +544,23 @@ public class GameManager : MonoBehaviour {
if(IsItemInvincibleActive())
{
SetTutorialGuide(TutorialGuideType.Items, "라멘", "무적 중이라 이번 충돌은 피해 없이 지나갑니다.");
if(ShouldShowItemEffectGuide())
{
SetTutorialGuide(TutorialGuideType.Items, "라멘", "무적 중이라 이번 충돌은 피해 없이 지나갑니다.");
}
SpawnItemBlockFeedback(TutorialItemType.InvincibleRamen);
return true;
}
if(shieldCharges > 0)
{
shieldCharges--;
SetTutorialGuide(TutorialGuideType.Items, "방어막", "방어막이 충돌 피해를 한 번 막았습니다.");
SpawnItemBlockFeedback(TutorialItemType.Shield);
if(ShouldShowItemEffectGuide())
{
SetTutorialGuide(TutorialGuideType.Items, "방어막", "방어막이 충돌 피해를 한 번 막았습니다.");
}
UpdateScoreUI();
return true;
}
@@ -405,6 +568,20 @@ public class GameManager : MonoBehaviour {
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;
}
@@ -425,12 +602,24 @@ public class GameManager : MonoBehaviour {
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;
@@ -444,6 +633,40 @@ public class GameManager : MonoBehaviour {
}
}
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)
{
@@ -518,6 +741,8 @@ public class GameManager : MonoBehaviour {
bool showItemRow = tutorialGuideType == TutorialGuideType.Items;
Sprite mainSprite = GetTutorialMainSprite(tutorialGuideType);
bool showFullText = !showItemRow && mainSprite == null;
UpdateTutorialTextLayout(showItemRow, showFullText);
if(tutorialMainImage != null)
{
@@ -542,6 +767,40 @@ public class GameManager : MonoBehaviour {
}
}
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)
{
@@ -726,22 +985,22 @@ public class GameManager : MonoBehaviour {
tutorialGuideObject.transform.SetParent(canvasTransform, false);
RectTransform guideRect = tutorialGuideObject.GetComponent<RectTransform>();
guideRect.anchorMin = new Vector2(0.5f, 0f);
guideRect.anchorMax = new Vector2(0.5f, 0f);
guideRect.pivot = new Vector2(0.5f, 0f);
guideRect.anchoredPosition = new Vector2(18f, 12f);
guideRect.sizeDelta = new Vector2(372f, 72f);
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.76f);
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(70f, -9f), new Vector2(286f, 18f), 12f, FontStyles.Bold);
tutorialBodyText = CreateTutorialText(tutorialGuideObject.transform, "Tutorial Body", new Vector2(70f, -28f), new Vector2(286f, 34f), 9f, FontStyles.Normal);
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);
}
@@ -936,4 +1195,413 @@ public class GameManager : MonoBehaviour {
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();
}
}
}
+316
View File
@@ -0,0 +1,316 @@
using TMPro;
using UnityEngine;
public class ItemFeedbackEffect : MonoBehaviour
{
private const string SortingLayerName = "Foreground";
private const float Lifetime = 0.85f;
private static Sprite ringSprite;
private static Sprite dotSprite;
private static Sprite sparkleSprite;
private SpriteRenderer ringRenderer;
private SpriteRenderer iconRenderer;
private SpriteRenderer[] sparkleRenderers;
private TextMeshPro labelText;
private Color mainColor;
private float elapsedTime;
public static void Spawn(Vector3 worldPosition, Sprite itemSprite, GameManager.TutorialItemType itemType, bool blockedDamage)
{
GameObject effectObject = new GameObject(blockedDamage ? "Item Block Feedback" : "Item Pickup Feedback");
effectObject.transform.position = worldPosition + new Vector3(0f, blockedDamage ? 1.1f : 0.85f, 0f);
ItemFeedbackEffect effect = effectObject.AddComponent<ItemFeedbackEffect>();
effect.Initialize(itemSprite, itemType, blockedDamage);
}
private void Initialize(Sprite itemSprite, GameManager.TutorialItemType itemType, bool blockedDamage)
{
mainColor = GetItemColor(itemType, blockedDamage);
sparkleRenderers = new SpriteRenderer[5];
ringRenderer = CreateSpriteChild("Ring", GetRingSprite(), Vector3.zero, 1.4f, 28);
iconRenderer = CreateSpriteChild("Icon", itemSprite != null ? itemSprite : GetDotSprite(), new Vector3(0f, 0.03f, 0f), itemSprite != null ? 0.52f : 0.72f, 29);
labelText = CreateLabel(GetLabel(itemType, blockedDamage));
for(int i = 0; i < sparkleRenderers.Length; i++)
{
float angle = (Mathf.PI * 2f / sparkleRenderers.Length) * i;
Vector3 localPosition = new Vector3(Mathf.Cos(angle) * 0.58f, Mathf.Sin(angle) * 0.38f, 0f);
sparkleRenderers[i] = CreateSpriteChild("Sparkle " + i, GetSparkleSprite(), localPosition, 0.18f, 30);
}
ApplyAlpha(1f);
}
private void Update()
{
elapsedTime += Time.unscaledDeltaTime;
float t = Mathf.Clamp01(elapsedTime / Lifetime);
float fade = 1f - Mathf.SmoothStep(0f, 1f, Mathf.InverseLerp(0.55f, 1f, t));
float popScale = Mathf.Lerp(0.72f, 1.16f, EaseOut(t));
transform.localScale = Vector3.one * popScale;
transform.position += Vector3.up * (Time.unscaledDeltaTime * 0.28f);
if(ringRenderer != null)
{
ringRenderer.transform.localScale = Vector3.one * Mathf.Lerp(0.82f, 1.38f, EaseOut(t));
}
for(int i = 0; i < sparkleRenderers.Length; i++)
{
if(sparkleRenderers[i] == null)
{
continue;
}
sparkleRenderers[i].transform.localScale = Vector3.one * Mathf.Lerp(0.16f, 0.32f, EaseOut(t));
sparkleRenderers[i].transform.Rotate(Vector3.forward, 180f * Time.unscaledDeltaTime);
}
ApplyAlpha(fade);
if(elapsedTime >= Lifetime)
{
Destroy(gameObject);
}
}
private SpriteRenderer CreateSpriteChild(string objectName, Sprite sprite, Vector3 localPosition, float scale, int sortingOrder)
{
GameObject child = new GameObject(objectName, typeof(SpriteRenderer));
child.transform.SetParent(transform, false);
child.transform.localPosition = localPosition;
child.transform.localScale = Vector3.one * scale;
SpriteRenderer spriteRenderer = child.GetComponent<SpriteRenderer>();
spriteRenderer.sprite = sprite;
spriteRenderer.color = mainColor;
spriteRenderer.sortingLayerName = SortingLayerName;
spriteRenderer.sortingOrder = sortingOrder;
return spriteRenderer;
}
private TextMeshPro CreateLabel(string text)
{
GameObject textObject = new GameObject("Label", typeof(TextMeshPro));
textObject.transform.SetParent(transform, false);
textObject.transform.localPosition = new Vector3(0f, 0.62f, 0f);
textObject.transform.localScale = Vector3.one * 0.28f;
TextMeshPro textMesh = textObject.GetComponent<TextMeshPro>();
textMesh.text = text;
textMesh.alignment = TextAlignmentOptions.Center;
textMesh.fontSize = 3.2f;
textMesh.fontStyle = FontStyles.Bold;
textMesh.color = Color.white;
textMesh.textWrappingMode = TextWrappingModes.NoWrap;
MeshRenderer textRenderer = textMesh.GetComponent<MeshRenderer>();
textRenderer.sortingLayerName = SortingLayerName;
textRenderer.sortingOrder = 31;
return textMesh;
}
private void ApplyAlpha(float alpha)
{
if(ringRenderer != null)
{
ringRenderer.color = WithAlpha(mainColor, alpha * 0.72f);
}
if(iconRenderer != null)
{
iconRenderer.color = WithAlpha(Color.white, alpha);
}
if(labelText != null)
{
labelText.color = WithAlpha(Color.white, alpha);
}
if(sparkleRenderers == null)
{
return;
}
for(int i = 0; i < sparkleRenderers.Length; i++)
{
if(sparkleRenderers[i] != null)
{
sparkleRenderers[i].color = WithAlpha(mainColor, alpha);
}
}
}
private static string GetLabel(GameManager.TutorialItemType itemType, bool blockedDamage)
{
if(blockedDamage)
{
return itemType == GameManager.TutorialItemType.InvincibleRamen ? "SAFE" : "BLOCK";
}
switch(itemType)
{
case GameManager.TutorialItemType.InvincibleRamen:
return "SAFE";
case GameManager.TutorialItemType.Shield:
return "SHIELD";
case GameManager.TutorialItemType.SpeedShoes:
return "SPEED";
case GameManager.TutorialItemType.MileageCard:
return "x2";
case GameManager.TutorialItemType.HealthSushi:
default:
return "+1";
}
}
private static Color GetItemColor(GameManager.TutorialItemType itemType, bool blockedDamage)
{
if(blockedDamage)
{
return itemType == GameManager.TutorialItemType.InvincibleRamen
? new Color(1f, 0.76f, 0.24f, 1f)
: new Color(0.36f, 0.82f, 1f, 1f);
}
switch(itemType)
{
case GameManager.TutorialItemType.InvincibleRamen:
return new Color(1f, 0.58f, 0.12f, 1f);
case GameManager.TutorialItemType.Shield:
return new Color(0.32f, 0.78f, 1f, 1f);
case GameManager.TutorialItemType.SpeedShoes:
return new Color(0.35f, 1f, 0.5f, 1f);
case GameManager.TutorialItemType.MileageCard:
return new Color(1f, 0.92f, 0.26f, 1f);
case GameManager.TutorialItemType.HealthSushi:
default:
return new Color(1f, 0.34f, 0.42f, 1f);
}
}
private static float EaseOut(float value)
{
return 1f - Mathf.Pow(1f - Mathf.Clamp01(value), 3f);
}
private static Color WithAlpha(Color color, float alpha)
{
return new Color(color.r, color.g, color.b, Mathf.Clamp01(alpha));
}
private static Sprite GetRingSprite()
{
if(ringSprite == null)
{
ringSprite = CreateRingSprite(64, 21f, 29f);
}
return ringSprite;
}
private static Sprite GetDotSprite()
{
if(dotSprite == null)
{
dotSprite = CreateDiscSprite(48, 19f);
}
return dotSprite;
}
private static Sprite GetSparkleSprite()
{
if(sparkleSprite == null)
{
sparkleSprite = CreateSparkleSprite(24);
}
return sparkleSprite;
}
private static Sprite CreateRingSprite(int size, float innerRadius, float outerRadius)
{
Texture2D texture = CreateTexture(size);
Color[] pixels = new Color[size * size];
Vector2 center = new Vector2((size - 1) * 0.5f, (size - 1) * 0.5f);
for(int y = 0; y < size; y++)
{
for(int x = 0; x < size; x++)
{
float distance = Vector2.Distance(new Vector2(x, y), center);
float outerAlpha = 1f - Mathf.SmoothStep(outerRadius - 2f, outerRadius, distance);
float innerAlpha = Mathf.SmoothStep(innerRadius - 2f, innerRadius, distance);
pixels[y * size + x] = new Color(1f, 1f, 1f, outerAlpha * innerAlpha);
}
}
texture.SetPixels(pixels);
texture.Apply();
return Sprite.Create(texture, new Rect(0f, 0f, size, size), new Vector2(0.5f, 0.5f), size);
}
private static Sprite CreateDiscSprite(int size, float radius)
{
Texture2D texture = CreateTexture(size);
Color[] pixels = new Color[size * size];
Vector2 center = new Vector2((size - 1) * 0.5f, (size - 1) * 0.5f);
for(int y = 0; y < size; y++)
{
for(int x = 0; x < size; x++)
{
float distance = Vector2.Distance(new Vector2(x, y), center);
float alpha = 1f - Mathf.SmoothStep(radius - 2f, radius, distance);
pixels[y * size + x] = new Color(1f, 1f, 1f, alpha);
}
}
texture.SetPixels(pixels);
texture.Apply();
return Sprite.Create(texture, new Rect(0f, 0f, size, size), new Vector2(0.5f, 0.5f), size);
}
private static Sprite CreateSparkleSprite(int size)
{
Texture2D texture = CreateTexture(size);
Color[] pixels = new Color[size * size];
float center = (size - 1) * 0.5f;
for(int y = 0; y < size; y++)
{
for(int x = 0; x < size; x++)
{
float horizontal = Mathf.Abs(y - center);
float vertical = Mathf.Abs(x - center);
float diagonalA = Mathf.Abs((x - center) - (y - center));
float diagonalB = Mathf.Abs((x - center) + (y - center));
float alpha = Mathf.Max(
1f - horizontal * 0.42f - Mathf.Abs(x - center) * 0.08f,
1f - vertical * 0.42f - Mathf.Abs(y - center) * 0.08f);
alpha = Mathf.Max(alpha, 0.62f - Mathf.Min(diagonalA, diagonalB) * 0.28f);
pixels[y * size + x] = new Color(1f, 1f, 1f, Mathf.Clamp01(alpha));
}
}
texture.SetPixels(pixels);
texture.Apply();
return Sprite.Create(texture, new Rect(0f, 0f, size, size), new Vector2(0.5f, 0.5f), size);
}
private static Texture2D CreateTexture(int size)
{
Texture2D texture = new Texture2D(size, size, TextureFormat.RGBA32, false);
texture.wrapMode = TextureWrapMode.Clamp;
texture.filterMode = FilterMode.Bilinear;
texture.hideFlags = HideFlags.HideAndDontSave;
return texture;
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 65d35f0ff5f04696b7f8c37a6c4e0979
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+1
View File
@@ -46,6 +46,7 @@ public class ItemPickup : MonoBehaviour
}
collected = true;
ItemFeedbackEffect.Spawn(transform.position, spriteRenderer != null ? spriteRenderer.sprite : null, itemType, false);
if(GameManager.instance != null)
{
+219 -46
View File
@@ -2,6 +2,12 @@ using UnityEngine;
public static class MapDatabase {
private static MapDefinition[] maps;
private const float GroundY = -2.4f;
private const float AerialY = -0.85f;
private const float BasicSpacing = 0.58f;
private const float ActionSpacing = 0.72f;
private const float LandingSpacing = 0.64f;
private const float AerialSpacing = 2.05f;
public static MapDefinition[] AllMaps {
get {
@@ -71,28 +77,44 @@ public static class MapDatabase {
displayName = "Tutorial / Incheon",
routeName = "인천공항 출발 준비",
isTutorial = true,
targetDistance = 850f,
timeLimit = 95f,
platformSkin = Platform.PlatformSkin.Airport,
backgroundResourcePath = "Map_Tutorial_Incheon_Airport",
finishGateStyle = FinishGateStyle.AirportDeparture,
targetDistance = 320f,
timeLimit = 75f,
clearMileageReward = 150,
hasNextMap = true,
nextMapId = MapId.Japan,
openingSteps = new PlatformSpawner.TutorialStep[] {
Step(Platform.PlatformPattern.LowMid, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.Jump, -2.4f, 1.05f, "점프", "가방 장애물이 보이면 왼쪽 클릭으로 뛰어넘습니다."),
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.4f, 1f),
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.DoubleJump, -2.4f, 2.1f, "2단 점프", "다음 발판이 멀리 있으면 공중에서 한 번 더 왼쪽 클릭합니다."),
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.4f, 1.1f),
Step(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.CenterPair, GameManager.TutorialGuideType.Slide, -2.4f, 1.15f, "슬라이드", "천장 표지판이 보이면 오른쪽 클릭을 누르고 지나갑니다."),
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.4f, 1f),
Step(Platform.PlatformPattern.ForceHit, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.WidePair, GameManager.TutorialGuideType.Damage, -2.4f, 1.15f, "피격", "이번 구간은 일부러 맞아 목숨과 속도 감소를 확인합니다."),
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.Items, -2.4f, 0.95f, "초밥", "초밥을 먹으면 잃은 목숨을 1 회복합니다.", Platform.ItemPattern.HealthSushi),
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.Items, -2.4f, 0.9f, "라멘", "라멘을 먹고 다음 충돌을 피해 없이 지나가 봅니다.", Platform.ItemPattern.InvincibleRamen),
Step(Platform.PlatformPattern.ForceHit, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.WidePair, GameManager.TutorialGuideType.None, -2.4f, 1.05f),
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.Items, -2.4f, 0.9f, "방어막", "방어막을 먹고 다음 충돌 1회를 막아 봅니다.", Platform.ItemPattern.Shield),
Step(Platform.PlatformPattern.ForceHit, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.WidePair, GameManager.TutorialGuideType.None, -2.4f, 1.05f),
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.Items, -2.4f, 0.95f, "운동화", "운동화는 속도를 회복해 일정이 늦어지는 것을 줄입니다.", Platform.ItemPattern.SpeedShoes),
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.Items, -2.4f, 0.95f, "마일리지 카드", "카드를 먹은 뒤 마일리지를 모으면 획득량이 늘어납니다.", Platform.ItemPattern.MileageCard),
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.4f, 1f),
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.4f, 1f)
Basic(),
Basic(),
Basic(GroundY, BasicSpacing),
Jump(GameManager.TutorialGuideType.Jump, "점프 구간", "가방 장애물이 오면 왼쪽 클릭으로 뛰어넘습니다."),
Basic(GroundY, LandingSpacing),
Basic(GroundY, BasicSpacing),
Aerial(GameManager.TutorialGuideType.DoubleJump, "2단 점프 구간", "다음 발판이 멀리 떨어져 있습니다.\n공중에서 왼쪽 클릭을 한 번 더 누릅니다."),
Basic(GroundY, LandingSpacing),
Basic(GroundY, BasicSpacing),
Slide(GameManager.TutorialGuideType.Slide, "슬라이드 구간", "천장 장애물이 오면 오른쪽 클릭을 누르고 지나갑니다."),
Basic(GroundY, LandingSpacing),
Basic(GroundY, BasicSpacing),
ForceHit(GameManager.TutorialGuideType.None, "", ""),
Basic(GroundY, LandingSpacing, Platform.MileagePattern.None, GameManager.TutorialGuideType.Items, "초밥", "잃은 목숨을 1 회복합니다.\n방금 줄어든 목숨을 채워 봅니다.", Platform.ItemPattern.HealthSushi),
Basic(GroundY, LandingSpacing),
Basic(GroundY, BasicSpacing, Platform.MileagePattern.None, GameManager.TutorialGuideType.Items, "라멘", "잠깐 무적이 됩니다.\n먹은 뒤 다음 장애물에 일부러 부딪혀 봅니다.", Platform.ItemPattern.InvincibleRamen),
ForceHit(GameManager.TutorialGuideType.None, "", ""),
Basic(GroundY, LandingSpacing),
Basic(GroundY, BasicSpacing, Platform.MileagePattern.None, GameManager.TutorialGuideType.Items, "방어막", "다음 충돌 1회를 막습니다.\n먹은 뒤 바로 충돌을 막아 봅니다.", Platform.ItemPattern.Shield),
ForceHit(GameManager.TutorialGuideType.None, "", ""),
Basic(GroundY, LandingSpacing),
ForceHit(GameManager.TutorialGuideType.None, "", ""),
Basic(GroundY, BasicSpacing, Platform.MileagePattern.None, GameManager.TutorialGuideType.Items, "운동화", "느려진 속도를 회복합니다.\n방금 떨어진 속도를 되돌려 봅니다.", Platform.ItemPattern.SpeedShoes),
Basic(GroundY, BasicSpacing, Platform.MileagePattern.None, GameManager.TutorialGuideType.Items, "마일리지 카드", "잠깐 마일리지가 2배가 됩니다.\n먹고 바로 마일리지를 모아 봅니다.", Platform.ItemPattern.MileageCard),
Basic(GroundY, BasicSpacing, Platform.MileagePattern.SafeLine),
Basic(),
Basic(),
Basic(),
Basic(GroundY, LandingSpacing)
},
loopSteps = CreateEasyLoop()
};
@@ -104,21 +126,28 @@ public static class MapDatabase {
displayName = "Japan",
routeName = "후쿠오카 라멘 투어",
isTutorial = false,
platformSkin = Platform.PlatformSkin.Japan,
backgroundResourcePath = "",
finishGateStyle = FinishGateStyle.JapanTorii,
targetDistance = 1200f,
timeLimit = 105f,
clearMileageReward = 250,
hasNextMap = true,
nextMapId = MapId.China,
openingSteps = new PlatformSpawner.TutorialStep[] {
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.2f, 1.8f)
Basic(),
Basic()
},
loopSteps = new PlatformSpawner.TutorialStep[] {
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.2f, 2.05f),
Step(Platform.PlatformPattern.LowMid, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.3f, 2.15f),
Step(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.CenterPair, GameManager.TutorialGuideType.None, -2.25f, 2.35f),
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -1.95f, 2.05f),
Step(Platform.PlatformPattern.LowLeft, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.35f, 2.1f),
Step(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.WidePair, GameManager.TutorialGuideType.None, -2.3f, 2.45f)
Basic(),
Jump(),
Basic(GroundY, LandingSpacing),
Aerial(),
Basic(GroundY, LandingSpacing),
Slide(SlideObstaclePattern.SlideLayout.CenterPair),
Basic(),
Jump(Platform.PlatformPattern.LowLeft),
Basic()
}
};
}
@@ -129,21 +158,29 @@ public static class MapDatabase {
displayName = "China",
routeName = "베이징 시장길",
isTutorial = false,
platformSkin = Platform.PlatformSkin.China,
backgroundResourcePath = "",
finishGateStyle = FinishGateStyle.ChinaPaifang,
targetDistance = 1450f,
timeLimit = 115f,
clearMileageReward = 350,
hasNextMap = true,
nextMapId = MapId.America,
openingSteps = new PlatformSpawner.TutorialStep[] {
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.1f, 1.9f)
Basic(),
Basic()
},
loopSteps = new PlatformSpawner.TutorialStep[] {
Step(Platform.PlatformPattern.LowRight, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.25f, 2.0f),
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -1.8f, 2.05f),
Step(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.LeftPair, GameManager.TutorialGuideType.None, -2.3f, 2.35f),
Step(Platform.PlatformPattern.LowMid, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.4f, 2.05f),
Step(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.RightPair, GameManager.TutorialGuideType.None, -2.25f, 2.35f),
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2f, 1.95f)
Basic(),
Jump(Platform.PlatformPattern.LowRight),
Basic(GroundY, LandingSpacing),
Slide(SlideObstaclePattern.SlideLayout.LeftPair),
Basic(),
Aerial(AerialY + 0.15f, 1.08f),
Basic(GroundY, LandingSpacing),
Jump(),
Basic(GroundY, LandingSpacing),
Slide(SlideObstaclePattern.SlideLayout.RightPair)
}
};
}
@@ -152,36 +189,166 @@ public static class MapDatabase {
return new MapDefinition {
mapId = MapId.America,
displayName = "America",
routeName = "LA 공항 고속도로",
routeName = "뉴욕 자유의 여신상",
isTutorial = false,
platformSkin = Platform.PlatformSkin.USA,
backgroundResourcePath = "Map_America_NewYork",
finishGateStyle = FinishGateStyle.AmericaRoadSign,
targetDistance = 1700f,
timeLimit = 125f,
clearMileageReward = 500,
hasNextMap = false,
nextMapId = MapId.America,
openingSteps = new PlatformSpawner.TutorialStep[] {
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.2f, 1.8f)
Basic(),
Basic()
},
loopSteps = new PlatformSpawner.TutorialStep[] {
Step(Platform.PlatformPattern.LowMid, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.4f, 1.95f),
Step(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.WidePair, GameManager.TutorialGuideType.None, -2.25f, 2.25f),
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -1.8f, 1.9f),
Step(Platform.PlatformPattern.LowLeft, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.35f, 1.95f),
Step(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.CenterPair, GameManager.TutorialGuideType.None, -2.25f, 2.2f),
Step(Platform.PlatformPattern.LowRight, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.1f, 1.9f)
Basic(),
Jump(Platform.PlatformPattern.LowMid),
Basic(GroundY, 0.86f),
Slide(SlideObstaclePattern.SlideLayout.WidePair),
Basic(),
Aerial(AerialY + 0.2f, 1.0f),
Basic(GroundY, 0.86f),
Jump(Platform.PlatformPattern.LowLeft),
Basic(),
Slide(SlideObstaclePattern.SlideLayout.CenterPair),
Basic(),
Jump(Platform.PlatformPattern.LowRight)
}
};
}
private static PlatformSpawner.TutorialStep[] CreateEasyLoop() {
return new PlatformSpawner.TutorialStep[] {
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.4f, 2.1f),
Step(Platform.PlatformPattern.LowMid, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.4f, 2.2f),
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.1f, 2.1f),
Step(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.CenterPair, GameManager.TutorialGuideType.None, -2.35f, 2.35f)
Basic(),
Jump(),
Basic(GroundY, LandingSpacing),
Aerial(),
Basic(GroundY, LandingSpacing),
Slide(SlideObstaclePattern.SlideLayout.CenterPair),
Basic()
};
}
private static PlatformSpawner.TutorialStep Basic(
float yPosition = GroundY,
float spawnInterval = BasicSpacing,
Platform.MileagePattern mileagePattern = Platform.MileagePattern.SafeLine,
GameManager.TutorialGuideType guideType = GameManager.TutorialGuideType.None,
string title = "",
string message = "",
Platform.ItemPattern itemPattern = Platform.ItemPattern.None) {
return Step(
Platform.PlatformPattern.Empty,
mileagePattern,
SlideObstaclePattern.SlideLayout.Random,
guideType,
yPosition,
spawnInterval,
title,
message,
itemPattern,
PlatformSpawner.CoursePlatformType.Basic);
}
private static PlatformSpawner.TutorialStep Jump(
Platform.PlatformPattern platformPattern = Platform.PlatformPattern.LowMid,
float spawnInterval = ActionSpacing,
GameManager.TutorialGuideType guideType = GameManager.TutorialGuideType.None,
string title = "",
string message = "") {
return Step(
platformPattern,
Platform.MileagePattern.JumpArc,
SlideObstaclePattern.SlideLayout.Random,
guideType,
GroundY,
spawnInterval,
title,
message,
Platform.ItemPattern.None,
PlatformSpawner.CoursePlatformType.Jump);
}
private static PlatformSpawner.TutorialStep Jump(
GameManager.TutorialGuideType guideType,
string title,
string message) {
return Jump(Platform.PlatformPattern.LowMid, ActionSpacing, guideType, title, message);
}
private static PlatformSpawner.TutorialStep Aerial(
GameManager.TutorialGuideType guideType = GameManager.TutorialGuideType.None,
string title = "",
string message = "") {
return Aerial(AerialY, AerialSpacing, guideType, title, message);
}
private static PlatformSpawner.TutorialStep Aerial(
float yPosition,
float spawnInterval,
GameManager.TutorialGuideType guideType = GameManager.TutorialGuideType.None,
string title = "",
string message = "") {
return Step(
Platform.PlatformPattern.Empty,
Platform.MileagePattern.SafeLine,
SlideObstaclePattern.SlideLayout.Random,
guideType,
GroundY,
spawnInterval,
title,
message,
Platform.ItemPattern.None,
PlatformSpawner.CoursePlatformType.Basic);
}
private static PlatformSpawner.TutorialStep Slide(
SlideObstaclePattern.SlideLayout slideLayout = SlideObstaclePattern.SlideLayout.CenterPair,
float spawnInterval = ActionSpacing,
GameManager.TutorialGuideType guideType = GameManager.TutorialGuideType.None,
string title = "",
string message = "") {
return Step(
Platform.PlatformPattern.Slide,
Platform.MileagePattern.SlideLine,
slideLayout,
guideType,
GroundY,
spawnInterval,
title,
message,
Platform.ItemPattern.None,
PlatformSpawner.CoursePlatformType.Slide);
}
private static PlatformSpawner.TutorialStep Slide(
GameManager.TutorialGuideType guideType,
string title,
string message) {
return Slide(SlideObstaclePattern.SlideLayout.CenterPair, ActionSpacing, guideType, title, message);
}
private static PlatformSpawner.TutorialStep ForceHit(
GameManager.TutorialGuideType guideType = GameManager.TutorialGuideType.Damage,
string title = "",
string message = "",
float spawnInterval = ActionSpacing) {
return Step(
Platform.PlatformPattern.ForceHit,
Platform.MileagePattern.None,
SlideObstaclePattern.SlideLayout.WidePair,
guideType,
GroundY,
spawnInterval,
title,
message,
Platform.ItemPattern.None,
PlatformSpawner.CoursePlatformType.ForceHit);
}
private static PlatformSpawner.TutorialStep Step(
Platform.PlatformPattern platformPattern,
Platform.MileagePattern mileagePattern,
@@ -191,8 +358,10 @@ public static class MapDatabase {
float spawnInterval,
string title = "",
string message = "",
Platform.ItemPattern itemPattern = Platform.ItemPattern.None) {
Platform.ItemPattern itemPattern = Platform.ItemPattern.None,
PlatformSpawner.CoursePlatformType coursePlatformType = PlatformSpawner.CoursePlatformType.Basic) {
return new PlatformSpawner.TutorialStep {
coursePlatformType = coursePlatformType,
platformPattern = platformPattern,
mileagePattern = mileagePattern,
slideLayout = slideLayout,
@@ -211,6 +380,9 @@ public static class MapDatabase {
displayName = source.displayName,
routeName = source.routeName,
isTutorial = source.isTutorial,
platformSkin = source.platformSkin,
backgroundResourcePath = source.backgroundResourcePath,
finishGateStyle = source.finishGateStyle,
targetDistance = source.targetDistance,
timeLimit = source.timeLimit,
clearMileageReward = source.clearMileageReward,
@@ -241,7 +413,8 @@ public static class MapDatabase {
sourceStep.spawnInterval,
sourceStep.title,
sourceStep.message,
sourceStep.itemPattern);
sourceStep.itemPattern,
sourceStep.coursePlatformType);
}
return steps;
+3
View File
@@ -6,6 +6,9 @@ public class MapDefinition {
public string displayName;
public string routeName;
public bool isTutorial;
public Platform.PlatformSkin platformSkin;
public string backgroundResourcePath;
public FinishGateStyle finishGateStyle;
public float targetDistance;
public float timeLimit;
public int clearMileageReward;
+205 -7
View File
@@ -32,6 +32,13 @@ public class Platform : MonoBehaviour {
MileageCard
}
public enum PlatformSkin {
Airport,
Japan,
China,
USA
}
public GameObject[] obstacles; // 장애물 오브젝트들
public int emptyPatternWeight = 2; // 장애물이 없는 패턴이 선택될 가중치
public Sprite mileageSprite; // 이 발판에서 사용할 마일리지 토큰 스프라이트
@@ -46,6 +53,7 @@ public class Platform : MonoBehaviour {
private MileagePattern reservedMileagePattern = MileagePattern.Random; // 다음 활성화 때 사용할 마일리지 패턴
private SlideObstaclePattern.SlideLayout reservedSlideLayout = SlideObstaclePattern.SlideLayout.Random; // 다음 슬라이딩 장애물 배치
private ItemPattern reservedItemPattern = ItemPattern.None; // 다음 활성화 때 사용할 아이템 배치
private PlatformSkin reservedSkin = PlatformSkin.Airport; // 현재 맵에서 사용할 발판/장애물 스킨
private const int MileagePoolSize = 5;
private const int ItemPoolSize = 1;
private const float MileageTokenScale = 0.55f;
@@ -57,18 +65,21 @@ public class Platform : MonoBehaviour {
PlatformPattern pattern,
MileagePattern mileagePattern,
SlideObstaclePattern.SlideLayout slideLayout = SlideObstaclePattern.SlideLayout.Random,
ItemPattern itemPattern = ItemPattern.None) {
ItemPattern itemPattern = ItemPattern.None,
PlatformSkin platformSkin = PlatformSkin.Airport) {
reservedPattern = pattern;
reservedMileagePattern = mileagePattern;
reservedSlideLayout = slideLayout;
reservedItemPattern = itemPattern;
reservedSkin = platformSkin;
}
public void ConfigureForRandom() {
public void ConfigureForRandom(PlatformSkin platformSkin = PlatformSkin.Airport) {
reservedPattern = PlatformPattern.Random;
reservedMileagePattern = MileagePattern.Random;
reservedSlideLayout = SlideObstaclePattern.SlideLayout.Random;
reservedItemPattern = ItemPattern.None;
reservedSkin = platformSkin;
}
// 컴포넌트가 활성화될때 마다 매번 실행되는 메서드
@@ -76,6 +87,7 @@ public class Platform : MonoBehaviour {
// 발판을 리셋하는 처리
stepped = false;
ApplyVisualSkin();
EnsureMileagePickups();
EnsureItemPickups();
@@ -361,23 +373,209 @@ public class Platform : MonoBehaviour {
}
private Vector2 GetItemPosition(ItemPattern itemPattern) {
const float frontX = -1.45f;
switch(itemPattern)
{
case ItemPattern.HealthSushi:
return new Vector2(-1.6f, 1.55f);
return new Vector2(frontX, 1.55f);
case ItemPattern.InvincibleRamen:
return new Vector2(-0.8f, 1.55f);
return new Vector2(frontX, 1.55f);
case ItemPattern.Shield:
return new Vector2(-0.2f, 1.55f);
return new Vector2(frontX, 1.55f);
case ItemPattern.SpeedShoes:
return new Vector2(0.7f, 1.55f);
return new Vector2(frontX, 1.55f);
case ItemPattern.MileageCard:
return new Vector2(1.4f, 1.55f);
return new Vector2(frontX, 1.55f);
default:
return new Vector2(0f, 1.55f);
}
}
private void ApplyVisualSkin() {
#if UNITY_EDITOR
Sprite platformSprite = LoadSpriteAtPath(GetPlatformSkinPath(reservedSkin));
Sprite ceilingSprite = LoadSpriteAtPath(GetCeilingSkinPath(reservedSkin));
Sprite lowObstacleSprite = LoadSpriteAtPath(GetLowObstacleSkinPath(reservedSkin));
Sprite hangingObstacleSprite = LoadSpriteAtPath(GetHangingObstacleSkinPath(reservedSkin));
Sprite mileageSkinSprite = LoadSpriteAtPath(GetMileageSkinPath(reservedSkin));
ApplySprite(GetComponent<SpriteRenderer>(), platformSprite);
Transform ceilingPlatform = FindChildRecursive(transform, "Ceiling Platform");
if(ceilingPlatform != null)
{
ApplySprite(ceilingPlatform.GetComponent<SpriteRenderer>(), ceilingSprite != null ? ceilingSprite : platformSprite);
}
ApplyLowObstacleSkin(lowObstacleSprite);
ApplyHangingObstacleSkin(hangingObstacleSprite);
if(mileageSkinSprite != null)
{
mileageSprite = mileageSkinSprite;
}
#endif
}
#if UNITY_EDITOR
private void ApplyLowObstacleSkin(Sprite obstacleSprite) {
if(obstacleSprite == null || obstacles == null)
{
return;
}
for(int i = 0; i < obstacles.Length; i++)
{
GameObject obstacle = obstacles[i];
if(obstacle == null || obstacle.name.Contains("Slide"))
{
continue;
}
ApplySprite(obstacle.GetComponent<SpriteRenderer>(), obstacleSprite);
}
}
private void ApplyHangingObstacleSkin(Sprite obstacleSprite) {
if(obstacleSprite == null)
{
return;
}
GameObject slideObstacle = FindObstacle("Slide Obstacle Pattern");
if(slideObstacle == null)
{
return;
}
SlideObstaclePattern slideObstaclePattern = slideObstacle.GetComponent<SlideObstaclePattern>();
if(slideObstaclePattern == null || slideObstaclePattern.obstacles == null)
{
return;
}
for(int i = 0; i < slideObstaclePattern.obstacles.Length; i++)
{
Transform hangingObstacle = slideObstaclePattern.obstacles[i];
if(hangingObstacle != null)
{
ApplySprite(hangingObstacle.GetComponent<SpriteRenderer>(), obstacleSprite);
}
}
}
private void ApplySprite(SpriteRenderer spriteRenderer, Sprite sprite) {
if(spriteRenderer != null && sprite != null)
{
spriteRenderer.sprite = sprite;
}
}
private Sprite LoadSpriteAtPath(string assetPath) {
if(string.IsNullOrEmpty(assetPath))
{
return null;
}
return AssetDatabase.LoadAssetAtPath<Sprite>(assetPath);
}
private Transform FindChildRecursive(Transform parent, string childName) {
if(parent == null)
{
return null;
}
for(int i = 0; i < parent.childCount; i++)
{
Transform child = parent.GetChild(i);
if(child.name == childName)
{
return child;
}
Transform match = FindChildRecursive(child, childName);
if(match != null)
{
return match;
}
}
return null;
}
private string GetPlatformSkinPath(PlatformSkin platformSkin) {
switch(platformSkin)
{
case PlatformSkin.Japan:
return "Assets/Sprites/ObstacleSkins/Skin_Platform_Japan.png";
case PlatformSkin.China:
return "Assets/Sprites/ObstacleSkins/Skin_Platform_China.png";
case PlatformSkin.USA:
return "Assets/Sprites/ObstacleSkins/Skin_Platform_USA.png";
case PlatformSkin.Airport:
default:
return "Assets/Sprites/ObstacleSkins/Skin_Platform_AirportFloorTile.png";
}
}
private string GetCeilingSkinPath(PlatformSkin platformSkin) {
if(platformSkin == PlatformSkin.Airport)
{
return "Assets/Sprites/ObstacleSkins/Skin_Platform_AirportCeiling.png";
}
return GetPlatformSkinPath(platformSkin);
}
private string GetLowObstacleSkinPath(PlatformSkin platformSkin) {
switch(platformSkin)
{
case PlatformSkin.Japan:
return "Assets/Sprites/ObstacleSkins/Skin_Obstacle_StoneLantern.png";
case PlatformSkin.China:
return "Assets/Sprites/ObstacleSkins/Skin_Obstacle_MarketCrate.png";
case PlatformSkin.USA:
return "Assets/Sprites/ObstacleSkins/Skin_Obstacle_Cone.png";
case PlatformSkin.Airport:
default:
return "Assets/Sprites/ObstacleSkins/Skin_Obstacle_Suitcases.png";
}
}
private string GetHangingObstacleSkinPath(PlatformSkin platformSkin) {
switch(platformSkin)
{
case PlatformSkin.Japan:
return "Assets/Sprites/ObstacleSkins/Skin_Hanging_RoadSign.png";
case PlatformSkin.China:
return "Assets/Sprites/ObstacleSkins/Skin_Hanging_RedLanterns.png";
case PlatformSkin.USA:
return "Assets/Sprites/ObstacleSkins/Skin_Hanging_Banner.png";
case PlatformSkin.Airport:
default:
return "Assets/Sprites/ObstacleSkins/Skin_Hanging_AirportSign.png";
}
}
private string GetMileageSkinPath(PlatformSkin platformSkin) {
switch(platformSkin)
{
case PlatformSkin.Japan:
return "Assets/Sprites/Mileage/Mileage_Japan.png";
case PlatformSkin.China:
return "Assets/Sprites/Mileage/Mileage_China.png";
case PlatformSkin.USA:
return "Assets/Sprites/Mileage/Mileage_USA.png";
case PlatformSkin.Airport:
default:
return "Assets/Sprites/Mileage/Mileage_Korea.png";
}
}
#endif
private Sprite LoadItemSprite(GameManager.TutorialItemType itemType) {
#if UNITY_EDITOR
string assetPath = "";
+365 -81
View File
@@ -1,9 +1,17 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using Random = UnityEngine.Random;
// 발판을 생성하고 주기적으로 재배치하는 스크립트
public class PlatformSpawner : MonoBehaviour {
public enum CoursePlatformType {
Basic,
Jump,
Slide,
ForceHit
}
private enum TutorialStepKind {
None,
Jump,
@@ -19,6 +27,7 @@ public class PlatformSpawner : MonoBehaviour {
[Serializable]
public class TutorialStep {
public CoursePlatformType coursePlatformType;
public Platform.PlatformPattern platformPattern;
public Platform.MileagePattern mileagePattern;
public SlideObstaclePattern.SlideLayout slideLayout;
@@ -30,25 +39,51 @@ public class PlatformSpawner : MonoBehaviour {
public string message;
}
public GameObject platformPrefab; // 생성할 발판의 원본 프리팹
public int count = 3; // 생성할 발판의 개수
private class PendingTutorialGuide {
public Transform target;
public GameManager.TutorialGuideType guideType;
public string title;
public string message;
public float triggerX;
}
public GameObject platformPrefab; // 계속 이어지는 기본 발판 프리팹
public GameObject jumpObstaclePrefab; // 낮은 점프 장애물 프리팹
public GameObject slideObstaclePrefab; // 위쪽 슬라이딩 장애물 프리팹
public int count = 12; // 기본 발판 풀 개수
public bool usePatternCourse = true; // 검증된 발판 패턴 코스를 사용
public bool tutorialMode = true; // 처음에는 튜토리얼 패턴을 먼저 배치
public TutorialStep[] tutorialSteps; // 튜토리얼 발판 순서
public TutorialStep[] stagePatternSteps; // 튜토리얼 이후 반복할 검증된 발판 순서
public bool keepWorldSpacingBySpeed = true; // 게임 속도가 빨라져도 발판 사이 실제 거리를 유지
public int continuousCoursePoolCount = 12; // 기본 발판을 촘촘히 깔 때 필요한 최소 풀 크기
public int obstaclePoolCount = 6; // 점프/슬라이드 장애물별 재사용 풀 크기
public float timeBetSpawnMin = 1.25f; // 다음 배치까지의 시간 간격 최솟값
public float timeBetSpawnMax = 2.25f; // 다음 배치까지의 시간 간격 최댓값
public float tutorialSpawnInterval = 2.2f; // 튜토리얼 발판 배치 간격
private float timeBetSpawn; // 다음 배치까지의 시간 간격
private const float FallbackGroundY = -2.4f;
private const float FallbackBasicSpacing = 0.58f;
private const float FallbackActionSpacing = 0.72f;
private const float FallbackLandingSpacing = 0.64f;
private const float FallbackAerialSpacing = 2.05f;
public float yMin = -3.5f; // 배치할 위치의 최소 y값
public float yMax = 1.5f; // 배치할 위치의 최대 y값
public float tutorialGuideTriggerX = 5f; // 발판/장애물이 이 위치까지 오면 설명을 띄운다.
private float xPos = 20f; // 배치할 위치의 x 값
private GameObject[] platforms; // 미리 생성한 발판
private int currentIndex = 0; // 사용할 현재 순번의 발판
private GameObject[] platforms; // 기본 발판
private GameObject[] jumpObstacles; // 점프 장애물 풀
private GameObject[] slideObstacles; // 슬라이드 장애물 풀
private int platformIndex = 0; // 기본 발판 풀 순번
private int jumpObstacleIndex = 0; // 점프 장애물 풀 순번
private int slideObstacleIndex = 0; // 슬라이드 장애물 풀 순번
private int courseStepIndex = 0; // 진행 중인 패턴 코스 순번
private Platform.PlatformSkin mapPlatformSkin = Platform.PlatformSkin.Airport; // 현재 맵에서 사용할 발판 스킨
private readonly List<PendingTutorialGuide> pendingTutorialGuides = new List<PendingTutorialGuide>(); // 실제 체험 직전에 띄울 설명들
private Vector2 poolPosition = new Vector2(0, -25); // 초반에 생성된 발판들을 화면 밖에 숨겨둘 위치
private float lastSpawnTime; // 마지막 배치 시점
@@ -61,24 +96,17 @@ public class PlatformSpawner : MonoBehaviour {
usePatternCourse = true;
tutorialMode = mapDefinition.isTutorial;
mapPlatformSkin = mapDefinition.platformSkin;
tutorialSteps = MapDatabase.CloneSteps(mapDefinition.openingSteps);
stagePatternSteps = MapDatabase.CloneSteps(mapDefinition.loopSteps);
courseStepIndex = 0;
currentIndex = 0;
pendingTutorialGuides.Clear();
ResetPoolIndices();
lastSpawnTime = Time.time;
timeBetSpawn = 0f;
if(platforms != null)
{
for(int i = 0; i < platforms.Length; i++)
{
if(platforms[i] != null)
{
platforms[i].SetActive(false);
platforms[i].transform.position = poolPosition;
}
}
}
count = Mathf.Max(count, continuousCoursePoolCount);
EnsurePlatformPool();
DeactivateAllPlatformPools();
}
void Start() {
@@ -86,16 +114,11 @@ public class PlatformSpawner : MonoBehaviour {
if(usePatternCourse && tutorialMode)
{
count = Mathf.Max(count, 8);
count = Mathf.Max(count, continuousCoursePoolCount);
}
// 변수들을 초기화하고 사용할 발판들을 미리 생성
platforms = new GameObject[count];
for(int i = 0; i < count; i++)
{
platforms[i] = Instantiate(platformPrefab, poolPosition, Quaternion.identity);
}
EnsurePlatformPool();
// 마지막 배치 시점 초기화
lastSpawnTime = 0f;
@@ -104,6 +127,77 @@ public class PlatformSpawner : MonoBehaviour {
timeBetSpawn = 0f;
}
private void EnsurePlatformPool() {
int basicCount = usePatternCourse ? Mathf.Max(count, continuousCoursePoolCount) : Mathf.Max(1, count);
int obstacleCount = Mathf.Max(1, obstaclePoolCount);
EnsurePool(ref platforms, basicCount, platformPrefab);
EnsurePool(ref jumpObstacles, obstacleCount, jumpObstaclePrefab);
EnsurePool(ref slideObstacles, obstacleCount, slideObstaclePrefab);
count = platforms != null ? platforms.Length : count;
}
private void EnsurePool(ref GameObject[] pool, int requiredCount, GameObject prefab) {
if(prefab == null)
{
return;
}
if(pool != null && pool.Length >= requiredCount)
{
return;
}
GameObject[] nextPool = new GameObject[requiredCount];
if(pool != null)
{
for(int i = 0; i < pool.Length; i++)
{
nextPool[i] = pool[i];
}
}
for(int i = 0; i < nextPool.Length; i++)
{
if(nextPool[i] == null)
{
nextPool[i] = Instantiate(prefab, poolPosition, Quaternion.identity);
nextPool[i].SetActive(false);
}
}
pool = nextPool;
}
private void ResetPoolIndices() {
platformIndex = 0;
jumpObstacleIndex = 0;
slideObstacleIndex = 0;
}
private void DeactivateAllPlatformPools() {
DeactivatePool(platforms);
DeactivatePool(jumpObstacles);
DeactivatePool(slideObstacles);
}
private void DeactivatePool(GameObject[] pool) {
if(pool == null)
{
return;
}
for(int i = 0; i < pool.Length; i++)
{
if(pool[i] != null)
{
pool[i].SetActive(false);
pool[i].transform.position = poolPosition;
}
}
}
void Update() {
// 순서를 돌아가며 주기적으로 발판을 배치
if(GameManager.instance == null || GameManager.instance.isGameover || !GameFlowManager.IsGameplayActive)
@@ -111,6 +205,14 @@ public class PlatformSpawner : MonoBehaviour {
return;
}
UpdatePendingTutorialGuides();
if(GameManager.instance != null && GameManager.instance.IsTutorialGuidePaused)
{
return;
}
EnsurePlatformPool();
if(Time.time >= lastSpawnTime + timeBetSpawn)
{
lastSpawnTime = Time.time;
@@ -122,19 +224,19 @@ public class PlatformSpawner : MonoBehaviour {
? Mathf.Clamp(step.yPosition, yMin, yMax)
: Random.Range(yMin, yMax);
platforms[currentIndex].SetActive(false);
ConfigurePlatform(platforms[currentIndex], step);
platforms[currentIndex].SetActive(true);
platforms[currentIndex].transform.position = new Vector2(xPos, yPos);
currentIndex++;
if(currentIndex >= count)
GameObject platformObject = TakeFromPool(platforms, ref platformIndex);
if(platformObject == null)
{
currentIndex = 0;
return;
}
platformObject.SetActive(false);
ConfigurePlatform(platformObject, step);
platformObject.SetActive(true);
platformObject.transform.position = new Vector2(xPos, yPos);
SpawnCourseObstacles(step, yPos);
RegisterTutorialGuide(platformObject.transform, step);
if(usePatternCourse)
{
courseStepIndex++;
@@ -142,6 +244,153 @@ public class PlatformSpawner : MonoBehaviour {
}
}
private void UpdatePendingTutorialGuides() {
if(GameManager.instance == null || pendingTutorialGuides.Count == 0 || GameManager.instance.IsTutorialGuidePaused)
{
return;
}
for(int i = 0; i < pendingTutorialGuides.Count; i++)
{
PendingTutorialGuide guide = pendingTutorialGuides[i];
if(guide == null || guide.target == null || !guide.target.gameObject.activeInHierarchy)
{
pendingTutorialGuides.RemoveAt(i);
i--;
continue;
}
if(guide.target.position.x <= guide.triggerX)
{
GameManager.instance.SetTutorialGuide(guide.guideType, guide.title, guide.message);
pendingTutorialGuides.RemoveAt(i);
return;
}
}
}
private void RegisterTutorialGuide(Transform target, TutorialStep step) {
if(target == null
|| step == null
|| step.guideType == GameManager.TutorialGuideType.None
|| (string.IsNullOrEmpty(step.title) && string.IsNullOrEmpty(step.message)))
{
return;
}
pendingTutorialGuides.Add(new PendingTutorialGuide {
target = target,
guideType = step.guideType,
title = step.title,
message = step.message,
triggerX = GetTutorialGuideTriggerX(step)
});
}
private float GetTutorialGuideTriggerX(TutorialStep step) {
if(step != null && step.guideType == GameManager.TutorialGuideType.DoubleJump)
{
return -2f;
}
return tutorialGuideTriggerX;
}
private void SpawnCourseObstacles(TutorialStep step, float yPos) {
if(step == null)
{
return;
}
if(step.coursePlatformType == CoursePlatformType.ForceHit || step.platformPattern == Platform.PlatformPattern.ForceHit)
{
SpawnJumpObstacle(Platform.PlatformPattern.LowMid, yPos);
SpawnSlideObstacle(SlideObstaclePattern.SlideLayout.WidePair, yPos);
return;
}
if(step.coursePlatformType == CoursePlatformType.Jump || IsJumpPattern(step.platformPattern))
{
Platform.PlatformPattern jumpPattern = IsJumpPattern(step.platformPattern)
? step.platformPattern
: Platform.PlatformPattern.LowMid;
SpawnJumpObstacle(jumpPattern, yPos);
return;
}
if(step.coursePlatformType == CoursePlatformType.Slide || step.platformPattern == Platform.PlatformPattern.Slide)
{
SpawnSlideObstacle(step.slideLayout, yPos);
}
}
private void SpawnJumpObstacle(Platform.PlatformPattern platformPattern, float yPos) {
GameObject obstacleObject = TakeFromPool(jumpObstacles, ref jumpObstacleIndex);
if(obstacleObject == null)
{
return;
}
obstacleObject.SetActive(false);
ConfigureObstacle(obstacleObject, platformPattern, SlideObstaclePattern.SlideLayout.Random);
obstacleObject.transform.position = new Vector2(xPos, yPos);
obstacleObject.SetActive(true);
}
private void SpawnSlideObstacle(SlideObstaclePattern.SlideLayout slideLayout, float yPos) {
GameObject obstacleObject = TakeFromPool(slideObstacles, ref slideObstacleIndex);
if(obstacleObject == null)
{
return;
}
obstacleObject.SetActive(false);
ConfigureObstacle(obstacleObject, Platform.PlatformPattern.Slide, slideLayout);
obstacleObject.transform.position = new Vector2(xPos, yPos);
obstacleObject.SetActive(true);
}
private void ConfigureObstacle(
GameObject obstacleObject,
Platform.PlatformPattern platformPattern,
SlideObstaclePattern.SlideLayout slideLayout) {
CourseObstacle obstacle = obstacleObject.GetComponent<CourseObstacle>();
if(obstacle != null)
{
obstacle.Configure(mapPlatformSkin, platformPattern, slideLayout);
}
}
private bool IsJumpPattern(Platform.PlatformPattern platformPattern) {
return platformPattern == Platform.PlatformPattern.LowLeft
|| platformPattern == Platform.PlatformPattern.LowMid
|| platformPattern == Platform.PlatformPattern.LowRight;
}
private GameObject TakeFromPool(GameObject[] pool, ref int poolIndex) {
if(pool == null || pool.Length == 0)
{
return null;
}
if(poolIndex < 0 || poolIndex >= pool.Length)
{
poolIndex = 0;
}
GameObject platformObject = pool[poolIndex];
poolIndex++;
if(poolIndex >= pool.Length)
{
poolIndex = 0;
}
return platformObject;
}
private void ConfigurePlatform(GameObject platformObject, TutorialStep step) {
Platform platform = platformObject.GetComponent<Platform>();
@@ -152,32 +401,33 @@ public class PlatformSpawner : MonoBehaviour {
if(step == null)
{
platform.ConfigureForRandom();
platform.ConfigureForRandom(mapPlatformSkin);
return;
}
platform.ConfigureForTutorial(step.platformPattern, step.mileagePattern, step.slideLayout, step.itemPattern);
platform.ConfigureForTutorial(Platform.PlatformPattern.Empty, step.mileagePattern, step.slideLayout, step.itemPattern, mapPlatformSkin);
if(GameManager.instance != null && step.guideType != GameManager.TutorialGuideType.None)
{
GameManager.instance.SetTutorialGuide(step.guideType, step.title, step.message);
}
if(GameManager.instance != null
&& step.guideType == GameManager.TutorialGuideType.None
&& (!tutorialMode || courseStepIndex >= tutorialSteps.Length))
{
GameManager.instance.SetTutorialGuide(GameManager.TutorialGuideType.None, "", "");
}
// 설명은 발판 생성 시점이 아니라 실제 체험 직전에 RegisterTutorialGuide가 띄운다.
}
private float GetStepSpawnInterval(TutorialStep step) {
float spawnInterval;
if(step != null && step.spawnInterval > 0f)
{
return step.spawnInterval;
spawnInterval = step.spawnInterval;
}
else
{
spawnInterval = tutorialSpawnInterval;
}
return tutorialSpawnInterval;
if(keepWorldSpacingBySpeed && GameManager.instance != null)
{
spawnInterval /= Mathf.Max(0.1f, GameManager.instance.gameSpeed);
}
return Mathf.Max(0.05f, spawnInterval);
}
private TutorialStep GetCurrentCourseStep() {
@@ -227,22 +477,22 @@ public class PlatformSpawner : MonoBehaviour {
}
tutorialSteps = new TutorialStep[] {
CreateTutorialStep(TutorialStepKind.Jump, Platform.PlatformPattern.LowMid, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, -2.4f, 1.05f),
CreateTutorialStep(TutorialStepKind.None, Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, -2.4f, 1f),
CreateTutorialStep(TutorialStepKind.DoubleJumpTakeoff, Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, -2.4f, 2.10f),
CreateTutorialStep(TutorialStepKind.None, Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, -2.4f, 1.10f),
CreateTutorialStep(TutorialStepKind.Slide, Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.CenterPair, -2.4f, 1.15f),
CreateTutorialStep(TutorialStepKind.None, Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, -2.4f, 1f),
CreateTutorialStep(TutorialStepKind.ForceHit, Platform.PlatformPattern.ForceHit, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.WidePair, -2.4f, 1.15f),
CreateTutorialStep(TutorialStepKind.HealthItem, Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, -2.4f, 0.95f, Platform.ItemPattern.HealthSushi),
CreateTutorialStep(TutorialStepKind.InvincibleItem, Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, -2.4f, 0.9f, Platform.ItemPattern.InvincibleRamen),
CreateTutorialStep(TutorialStepKind.None, Platform.PlatformPattern.ForceHit, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.WidePair, -2.4f, 1.05f),
CreateTutorialStep(TutorialStepKind.ShieldItem, Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, -2.4f, 0.9f, Platform.ItemPattern.Shield),
CreateTutorialStep(TutorialStepKind.None, Platform.PlatformPattern.ForceHit, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.WidePair, -2.4f, 1.05f),
CreateTutorialStep(TutorialStepKind.SpeedItem, Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, -2.4f, 0.95f, Platform.ItemPattern.SpeedShoes),
CreateTutorialStep(TutorialStepKind.MileageCardItem, Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, -2.4f, 0.95f, Platform.ItemPattern.MileageCard),
CreateTutorialStep(TutorialStepKind.None, Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, -2.4f, 1f),
CreateTutorialStep(TutorialStepKind.None, Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, -2.4f, 1f),
CreateTutorialStep(TutorialStepKind.Jump, Platform.PlatformPattern.LowMid, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, FallbackGroundY, FallbackActionSpacing),
CreateTutorialStep(TutorialStepKind.None, Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, FallbackGroundY, FallbackLandingSpacing),
CreateTutorialStep(TutorialStepKind.DoubleJumpTakeoff, Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, FallbackGroundY, FallbackAerialSpacing),
CreateTutorialStep(TutorialStepKind.None, Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, FallbackGroundY, FallbackLandingSpacing),
CreateTutorialStep(TutorialStepKind.Slide, Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.CenterPair, FallbackGroundY, FallbackActionSpacing),
CreateTutorialStep(TutorialStepKind.None, Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, FallbackGroundY, FallbackLandingSpacing),
CreateTutorialStep(TutorialStepKind.ForceHit, Platform.PlatformPattern.ForceHit, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.WidePair, FallbackGroundY, FallbackActionSpacing),
CreateTutorialStep(TutorialStepKind.HealthItem, Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, FallbackGroundY, FallbackLandingSpacing, Platform.ItemPattern.HealthSushi),
CreateTutorialStep(TutorialStepKind.InvincibleItem, Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, FallbackGroundY, FallbackBasicSpacing, Platform.ItemPattern.InvincibleRamen),
CreateTutorialStep(TutorialStepKind.None, Platform.PlatformPattern.ForceHit, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.WidePair, FallbackGroundY, FallbackActionSpacing),
CreateTutorialStep(TutorialStepKind.ShieldItem, Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, FallbackGroundY, FallbackBasicSpacing, Platform.ItemPattern.Shield),
CreateTutorialStep(TutorialStepKind.None, Platform.PlatformPattern.ForceHit, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.WidePair, FallbackGroundY, FallbackActionSpacing),
CreateTutorialStep(TutorialStepKind.SpeedItem, Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, FallbackGroundY, FallbackBasicSpacing, Platform.ItemPattern.SpeedShoes),
CreateTutorialStep(TutorialStepKind.MileageCardItem, Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, FallbackGroundY, FallbackBasicSpacing, Platform.ItemPattern.MileageCard),
CreateTutorialStep(TutorialStepKind.None, Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, FallbackGroundY, FallbackBasicSpacing),
CreateTutorialStep(TutorialStepKind.None, Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, FallbackGroundY, FallbackBasicSpacing),
};
}
@@ -253,20 +503,20 @@ public class PlatformSpawner : MonoBehaviour {
}
stagePatternSteps = new TutorialStep[] {
CreateStep(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.2f, 2.25f, "", ""),
CreateStep(Platform.PlatformPattern.LowMid, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.3f, 2.4f, "", ""),
CreateStep(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -1.95f, 2.2f, "", ""),
CreateStep(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.WidePair, GameManager.TutorialGuideType.None, -2.25f, 2.65f, "", ""),
CreateStep(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.0f, 2.2f, "", ""),
CreateStep(Platform.PlatformPattern.LowLeft, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.35f, 2.35f, "", ""),
CreateStep(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.LeftPair, GameManager.TutorialGuideType.None, -2.35f, 2.7f, "", ""),
CreateStep(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.1f, 2.2f, "", ""),
CreateStep(Platform.PlatformPattern.LowRight, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.0f, 2.35f, "", ""),
CreateStep(Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -1.85f, 2.15f, "", ""),
CreateStep(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.RightPair, GameManager.TutorialGuideType.None, -2.25f, 2.65f, "", ""),
CreateStep(Platform.PlatformPattern.LowMid, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.4f, 2.35f, "", ""),
CreateStep(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.0f, 2.2f, "", ""),
CreateStep(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.CenterPair, GameManager.TutorialGuideType.None, -2.2f, 2.7f, "", ""),
CreateStep(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, FallbackGroundY, FallbackBasicSpacing, "", ""),
CreateStep(Platform.PlatformPattern.LowMid, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, FallbackGroundY, FallbackActionSpacing, "", "", Platform.ItemPattern.None, CoursePlatformType.Jump),
CreateStep(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, FallbackGroundY, FallbackLandingSpacing, "", ""),
CreateStep(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.WidePair, GameManager.TutorialGuideType.None, FallbackGroundY, FallbackActionSpacing, "", "", Platform.ItemPattern.None, CoursePlatformType.Slide),
CreateStep(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, FallbackGroundY, FallbackBasicSpacing, "", ""),
CreateStep(Platform.PlatformPattern.LowLeft, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, FallbackGroundY, FallbackActionSpacing, "", "", Platform.ItemPattern.None, CoursePlatformType.Jump),
CreateStep(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.LeftPair, GameManager.TutorialGuideType.None, FallbackGroundY, FallbackActionSpacing, "", "", Platform.ItemPattern.None, CoursePlatformType.Slide),
CreateStep(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, FallbackGroundY, FallbackBasicSpacing, "", ""),
CreateStep(Platform.PlatformPattern.LowRight, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, FallbackGroundY, FallbackActionSpacing, "", "", Platform.ItemPattern.None, CoursePlatformType.Jump),
CreateStep(Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, FallbackGroundY, FallbackAerialSpacing, "", ""),
CreateStep(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.RightPair, GameManager.TutorialGuideType.None, FallbackGroundY, FallbackActionSpacing, "", "", Platform.ItemPattern.None, CoursePlatformType.Slide),
CreateStep(Platform.PlatformPattern.LowMid, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, FallbackGroundY, FallbackActionSpacing, "", "", Platform.ItemPattern.None, CoursePlatformType.Jump),
CreateStep(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, FallbackGroundY, FallbackBasicSpacing, "", ""),
CreateStep(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.CenterPair, GameManager.TutorialGuideType.None, FallbackGroundY, FallbackActionSpacing, "", "", Platform.ItemPattern.None, CoursePlatformType.Slide),
};
}
@@ -279,8 +529,10 @@ public class PlatformSpawner : MonoBehaviour {
float spawnInterval,
string title,
string message,
Platform.ItemPattern itemPattern = Platform.ItemPattern.None) {
Platform.ItemPattern itemPattern = Platform.ItemPattern.None,
CoursePlatformType coursePlatformType = CoursePlatformType.Basic) {
TutorialStep step = new TutorialStep();
step.coursePlatformType = coursePlatformType;
step.platformPattern = platformPattern;
step.mileagePattern = mileagePattern;
step.slideLayout = slideLayout;
@@ -354,6 +606,38 @@ public class PlatformSpawner : MonoBehaviour {
break;
}
return CreateStep(platformPattern, mileagePattern, slideLayout, guideType, yPosition, spawnInterval, title, message, itemPattern);
return CreateStep(
platformPattern,
mileagePattern,
slideLayout,
guideType,
yPosition,
spawnInterval,
title,
message,
itemPattern,
GetCoursePlatformType(stepKind, platformPattern, yPosition));
}
private CoursePlatformType GetCoursePlatformType(TutorialStepKind stepKind, Platform.PlatformPattern platformPattern, float yPosition) {
if(stepKind == TutorialStepKind.ForceHit || platformPattern == Platform.PlatformPattern.ForceHit)
{
return CoursePlatformType.ForceHit;
}
if(stepKind == TutorialStepKind.Slide || platformPattern == Platform.PlatformPattern.Slide)
{
return CoursePlatformType.Slide;
}
if(stepKind == TutorialStepKind.Jump
|| platformPattern == Platform.PlatformPattern.LowLeft
|| platformPattern == Platform.PlatformPattern.LowMid
|| platformPattern == Platform.PlatformPattern.LowRight)
{
return CoursePlatformType.Jump;
}
return CoursePlatformType.Basic;
}
}
+44 -11
View File
@@ -1,4 +1,5 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// PlayerController는 플레이어 캐릭터로서 Player 게임 오브젝트를 제어한다.
@@ -7,6 +8,7 @@ public class PlayerController : MonoBehaviour {
public float jumpForce = 700f; // 점프 힘
public int maxJumpCount = 2; // 최대 점프 횟수
public float invincibleDuration = 1.5f; // 피해를 입은 뒤 무적 시간
public float groundStickGraceTime = 0.12f; // 발판 이음새에서 접지가 아주 잠깐 끊겨도 유지하는 시간
private int jumpCount = 0; // 누적 점프 횟수
private bool isGrounded = false; // 바닥에 닿았는지 나타냄
@@ -18,6 +20,8 @@ public class PlayerController : MonoBehaviour {
private Animator animator; // 사용할 애니메이터 컴포넌트
private AudioSource playerAudio; // 사용할 오디오 소스 컴포넌트
private SpriteRenderer spriteRenderer; // 플레이어 스프라이트 표시 컴포넌트
private readonly HashSet<Collider2D> groundContacts = new HashSet<Collider2D>(); // 현재 밟고 있는 발판 콜라이더
private float lastGroundedTime = -999f; // 마지막으로 발판에 닿아 있던 시간
private CapsuleCollider2D playerCollider; // 플레이어의 콜라이더 충돌 범위
private Vector2 originalColliderSize; // 기본 자세일 때 콜라이더 크기
@@ -42,7 +46,9 @@ public class PlayerController : MonoBehaviour {
}
private void Update() {
if(isDead || !GameFlowManager.IsGameplayActive)
if(isDead
|| !GameFlowManager.IsGameplayActive
|| (GameManager.instance != null && GameManager.instance.IsTutorialGuidePaused))
{
return;
}
@@ -67,14 +73,15 @@ public class PlayerController : MonoBehaviour {
playerRigidbody.linearVelocity = playerRigidbody.linearVelocity * 0.5f;
}
animator.SetBool("Grounded", isGrounded);
bool groundedForMovement = IsGroundedOrRecentlyGrounded();
animator.SetBool("Grounded", groundedForMovement);
// 달리는 중에 마우스 오른쪽 버튼을 누르고 있는 동안 슬라이딩 유지
if(Input.GetMouseButton(1) && isGrounded && !isSliding)
if(Input.GetMouseButton(1) && groundedForMovement && !isSliding)
{
StartSlide();
}
else if((!Input.GetMouseButton(1) || !isGrounded) && isSliding)
else if((!Input.GetMouseButton(1) || !groundedForMovement) && isSliding)
{
EndSlide();
}
@@ -127,23 +134,43 @@ public class PlayerController : MonoBehaviour {
private void OnCollisionEnter2D(Collision2D collision) {
// 어떤 콜라이더와 닿았으며, 충돌 표면이 위쪽을 보고 있으면
if(collision.contacts[0].normal.y > 0.7f)
if(IsGroundContact(collision))
{
isGrounded = true;
groundContacts.Add(collision.collider);
SetGrounded(true);
jumpCount = 0;
}
}
private void OnCollisionExit2D(Collision2D collision) {
// 바닥에서 벗어났음을 감지하는 처리
isGrounded = false;
if(isSliding)
if(groundContacts.Remove(collision.collider) && groundContacts.Count == 0)
{
EndSlide();
SetGrounded(false);
}
}
private bool IsGroundContact(Collision2D collision) {
for(int i = 0; i < collision.contactCount; i++)
{
if(collision.GetContact(i).normal.y > 0.7f)
{
return true;
}
}
return false;
}
private void SetGrounded(bool grounded) {
isGrounded = grounded;
lastGroundedTime = Time.time;
}
private bool IsGroundedOrRecentlyGrounded() {
return isGrounded || Time.time - lastGroundedTime <= groundStickGraceTime;
}
private void StartSlide() {
isSliding = true;
animator.SetBool("Sliding", true);
@@ -170,7 +197,7 @@ public class PlayerController : MonoBehaviour {
if(GameManager.instance != null && GameManager.instance.TryBlockDamageWithItem())
{
StartCoroutine(InvincibleRoutine());
StartCoroutine(DamageBlockCooldownRoutine());
return;
}
@@ -185,6 +212,12 @@ public class PlayerController : MonoBehaviour {
StartCoroutine(InvincibleRoutine());
}
private IEnumerator DamageBlockCooldownRoutine() {
isInvincible = true;
yield return new WaitForSeconds(0.35f);
isInvincible = false;
}
private IEnumerator InvincibleRoutine() {
isInvincible = true;