2026-06-19 UI, UI로직

This commit is contained in:
skrwns304@gmail.com
2026-06-19 14:27:40 +09:00
parent b751a9ed66
commit b1e85a5b89
549 changed files with 18058 additions and 20 deletions

View File

@@ -0,0 +1,358 @@
using System.Collections;
using UnityEngine;
using UnityEngine.InputSystem;
public class FishingGameManager : MonoBehaviour
{
public enum ResultType
{
Perfect,
Good,
Miss
}
[Header("References")]
[SerializeField] private FishingGaugeUI ui;
[SerializeField] private FishingRewardSystem rewardSystem;
[SerializeField] private FishingHapticManager haptic;
[SerializeField] private RotateUI centerIconRotate;
[Header("Pointer")]
[SerializeField] private float pointerAngle;
[SerializeField] private float pointerSpeed = 180f;
[SerializeField] private float minSpeed = 180f;
[SerializeField] private float maxSpeed = 450f;
[Header("Zone")]
[SerializeField] private float startZoneSize = 90f;
[SerializeField] private float zoneSize = 90f;
[SerializeField] private float minZoneSize = 30f;
[SerializeField] private float maxZoneSize = 120f;
[SerializeField] private float zoneCenter;
[Header("Rules")]
[SerializeField] private int requiredSuccesses = 3;
[SerializeField] private int allowedFails = 3;
[Header("Round Settings")]
[SerializeField] private float nextRoundDelay = 0.35f;
[SerializeField] private bool randomDirectionEachRound = true;
[SerializeField] private bool startOnAwake = true;
[Header("Debug")]
[SerializeField] private bool showDebugLog = true;
private int successCount;
private int failCount;
private bool clockwise = true;
private bool activeGame;
private bool inputLocked;
private Coroutine nextRoundRoutine;
private void Start()
{
if (startOnAwake)
{
StartFishing();
}
else
{
InitializeIdleUI();
}
}
private void Update()
{
if (!activeGame)
return;
if (inputLocked)
return;
RotatePointer();
}
private void InitializeIdleUI()
{
if (ui != null)
{
ui.HideRoundResult();
ui.HideFinalResult();
ui.UpdateCounter(0, requiredSuccesses, 0, allowedFails);
ui.SetPointerRotation(0f);
ui.SetZone(0f, startZoneSize);
}
if (centerIconRotate != null)
centerIconRotate.StopRotate();
}
public void StartFishing()
{
if (nextRoundRoutine != null)
{
StopCoroutine(nextRoundRoutine);
nextRoundRoutine = null;
}
successCount = 0;
failCount = 0;
pointerAngle = 0f;
pointerSpeed = minSpeed;
zoneSize = startZoneSize;
activeGame = true;
inputLocked = false;
clockwise = Random.value > 0.5f;
if (ui != null)
{
ui.HideRoundResult();
ui.HideFinalResult();
}
if (centerIconRotate != null)
{
centerIconRotate.ResetRotation();
centerIconRotate.StartRotate();
}
RandomizeZone();
UpdateUI();
if (showDebugLog)
Debug.Log("낚시 시작");
}
private void RotatePointer()
{
float dir = clockwise ? 1f : -1f;
pointerAngle += dir * pointerSpeed * Time.deltaTime;
pointerAngle = Normalize(pointerAngle);
if (ui != null)
ui.SetPointerRotation(pointerAngle);
}
public void SubmitAttempt()
{
if (!activeGame)
return;
if (inputLocked)
return;
inputLocked = true;
ResultType result = EvaluateResult();
switch (result)
{
case ResultType.Perfect:
OnPerfect();
break;
case ResultType.Good:
OnGood();
break;
case ResultType.Miss:
OnMiss();
break;
}
if (successCount >= requiredSuccesses)
{
FishingSuccess();
return;
}
if (failCount >= allowedFails)
{
FishingFail();
return;
}
nextRoundRoutine = StartCoroutine(NextRoundRoutine());
}
private ResultType EvaluateResult()
{
float delta = Mathf.Abs(Mathf.DeltaAngle(pointerAngle, zoneCenter));
float perfectRange = zoneSize * 0.2f;
float goodRange = zoneSize * 0.5f;
if (delta <= perfectRange)
return ResultType.Perfect;
if (delta <= goodRange)
return ResultType.Good;
return ResultType.Miss;
}
private void OnPerfect()
{
successCount++;
zoneSize *= 0.9f;
pointerSpeed *= 1.05f;
if (haptic != null)
haptic.Perfect();
if (ui != null)
ui.ShowResult("완벽!");
if (showDebugLog)
Debug.Log("낚시 판정: Perfect");
ClampDifficulty();
UpdateUI();
}
private void OnGood()
{
successCount++;
zoneSize *= 0.95f;
pointerSpeed *= 1.02f;
if (haptic != null)
haptic.Good();
if (ui != null)
ui.ShowResult("성공!");
if (showDebugLog)
Debug.Log("낚시 판정: Good");
ClampDifficulty();
UpdateUI();
}
private void OnMiss()
{
failCount++;
zoneSize *= 1.05f;
pointerSpeed *= 0.95f;
if (haptic != null)
haptic.Miss();
if (ui != null)
ui.ShowResult("실패!");
if (showDebugLog)
Debug.Log("낚시 판정: Miss");
ClampDifficulty();
UpdateUI();
}
private IEnumerator NextRoundRoutine()
{
yield return new WaitForSeconds(nextRoundDelay);
if (!activeGame)
yield break;
if (randomDirectionEachRound)
clockwise = Random.value > 0.5f;
RandomizeZone();
UpdateUI();
inputLocked = false;
nextRoundRoutine = null;
}
private void ClampDifficulty()
{
zoneSize = Mathf.Clamp(zoneSize, minZoneSize, maxZoneSize);
pointerSpeed = Mathf.Clamp(pointerSpeed, minSpeed, maxSpeed);
}
private void RandomizeZone()
{
zoneCenter = Random.Range(0f, 360f);
}
private void UpdateUI()
{
if (ui == null)
return;
ui.SetZone(zoneCenter, zoneSize);
ui.UpdateCounter(successCount, requiredSuccesses, failCount, allowedFails);
}
private void FishingSuccess()
{
activeGame = false;
inputLocked = true;
if (centerIconRotate != null)
centerIconRotate.StopRotate();
if (ui != null)
ui.ShowFinalResult("낚시 성공!");
if (rewardSystem != null)
rewardSystem.GiveReward();
if (showDebugLog)
Debug.Log("낚시 최종 성공");
}
private void FishingFail()
{
activeGame = false;
inputLocked = true;
if (centerIconRotate != null)
centerIconRotate.StopRotate();
if (ui != null)
ui.ShowFinalResult("낚시 실패!");
if (showDebugLog)
Debug.Log("낚시 최종 실패");
}
public void ResetFishing()
{
StartFishing();
}
private float Normalize(float angle)
{
angle %= 360f;
if (angle < 0f)
angle += 360f;
return angle;
}
// XR Input Action의 Select / Trigger 버튼에 연결
public void OnSubmit(InputValue value)
{
if (value.isPressed)
SubmitAttempt();
}
// Reset 버튼에 연결 가능
public void OnReset(InputValue value)
{
if (value.isPressed)
ResetFishing();
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 64a24b6b9056f714b9641eb630b4ed0f

View File

@@ -0,0 +1,155 @@
using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class FishingGaugeUI : MonoBehaviour
{
[Header("Panel UI")]
[SerializeField] private GameObject counterPanel;
[SerializeField] private GameObject finalResultPanel;
[Header("Gauge UI")]
[SerializeField] private RectTransform pointerPivot;
[SerializeField] private Image successZone;
[Header("Text UI")]
[SerializeField] private TMP_Text successText;
[SerializeField] private TMP_Text failText;
[SerializeField] private TMP_Text resultText;
[SerializeField] private TMP_Text finalResultText;
[Header("Zone Visual Correction")]
[Tooltip("성공 구간 이미지가 판정 위치와 어긋나면 이 값을 조정하세요.")]
[SerializeField] private float zoneVisualOffset = 0f;
[Header("Result Settings")]
[SerializeField] private float resultShowTime = 1f;
[SerializeField] private bool hideCounterOnFinalResult = true;
private Coroutine resultRoutine;
private void Awake()
{
HideRoundResult();
HideFinalResult();
ShowCounter();
}
public void SetPointerRotation(float angle)
{
if (pointerPivot == null)
return;
pointerPivot.localEulerAngles = new Vector3(0f, 0f, -angle);
}
public void SetZone(float centerAngle, float size)
{
SetZoneSize(size);
SetZoneRotation(centerAngle, size);
}
public void SetZoneSize(float size)
{
if (successZone == null)
return;
successZone.fillAmount = Mathf.Clamp01(size / 360f);
}
public void SetZoneRotation(float centerAngle, float size)
{
if (successZone == null)
return;
float startAngle = centerAngle - size * 0.5f + zoneVisualOffset;
successZone.rectTransform.localEulerAngles =
new Vector3(0f, 0f, -startAngle);
}
public void UpdateCounter(int success, int successTarget, int fail, int failTarget)
{
ShowCounter();
if (successText != null)
successText.text = $"성공 {success}/{successTarget}";
if (failText != null)
failText.text = $"실패 {fail}/{failTarget}";
}
public void ShowCounter()
{
if (counterPanel != null)
counterPanel.SetActive(true);
}
public void HideCounter()
{
if (counterPanel != null)
counterPanel.SetActive(false);
}
public void ShowResult(string text)
{
if (resultText == null)
return;
if (resultRoutine != null)
StopCoroutine(resultRoutine);
resultRoutine = StartCoroutine(ResultRoutine(text));
}
private IEnumerator ResultRoutine(string text)
{
resultText.gameObject.SetActive(true);
resultText.text = text;
yield return new WaitForSeconds(resultShowTime);
resultText.gameObject.SetActive(false);
resultRoutine = null;
}
public void HideRoundResult()
{
if (resultRoutine != null)
{
StopCoroutine(resultRoutine);
resultRoutine = null;
}
if (resultText != null)
resultText.gameObject.SetActive(false);
}
public void ShowFinalResult(string text)
{
HideRoundResult();
if (hideCounterOnFinalResult)
HideCounter();
if (finalResultPanel != null)
finalResultPanel.SetActive(true);
if (finalResultText != null)
{
finalResultText.gameObject.SetActive(true);
finalResultText.text = text;
}
}
public void HideFinalResult()
{
if (finalResultPanel != null)
finalResultPanel.SetActive(false);
if (finalResultText != null)
finalResultText.gameObject.SetActive(false);
ShowCounter();
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 17b5efd201ace6b4c9a0af3b6c9b6c2b

View File

@@ -0,0 +1,54 @@
using UnityEngine;
using UnityEngine.XR;
public class FishingHapticManager : MonoBehaviour
{
[Header("Haptic Target")]
[SerializeField] private XRNode targetHand = XRNode.RightHand;
[Header("Haptic Settings")]
[SerializeField] private bool useHaptic = true;
[SerializeField] private float perfectAmplitude = 1f;
[SerializeField] private float perfectDuration = 0.2f;
[SerializeField] private float goodAmplitude = 0.5f;
[SerializeField] private float goodDuration = 0.1f;
[SerializeField] private float missAmplitude = 0.2f;
[SerializeField] private float missDuration = 0.05f;
private void SendHaptic(float amplitude, float duration)
{
if (!useHaptic)
return;
InputDevice device = InputDevices.GetDeviceAtXRNode(targetHand);
if (!device.isValid)
return;
if (device.TryGetHapticCapabilities(out HapticCapabilities capabilities))
{
if (!capabilities.supportsImpulse)
return;
}
device.SendHapticImpulse(0u, Mathf.Clamp01(amplitude), duration);
}
public void Perfect()
{
SendHaptic(perfectAmplitude, perfectDuration);
}
public void Good()
{
SendHaptic(goodAmplitude, goodDuration);
}
public void Miss()
{
SendHaptic(missAmplitude, missDuration);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c4126d14d0a225c4b83981d97a52f63d

View File

@@ -0,0 +1,53 @@
using UnityEngine;
public class FishingRewardSystem : MonoBehaviour
{
public enum RewardType
{
Normal,
Rare
}
[Header("Reward Chance")]
[Range(0f, 100f)]
[SerializeField] private float rareChance = 10f;
[Header("Reward Count")]
[SerializeField] private int normalMemory;
[SerializeField] private int rareMemory;
public int NormalMemory => normalMemory;
public int RareMemory => rareMemory;
public RewardType GiveReward()
{
if (Random.value < rareChance / 100f)
return GiveRareMemory();
return GiveNormalMemory();
}
private RewardType GiveNormalMemory()
{
normalMemory++;
Debug.Log("ÀÏ¹Ý ±â¾ï +1");
return RewardType.Normal;
}
private RewardType GiveRareMemory()
{
rareMemory++;
Debug.Log("Èñ±Í ±â¾ï +1");
return RewardType.Rare;
}
public void ResetRewardCount()
{
normalMemory = 0;
rareMemory = 0;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 924b1cf99b7e65c4cbeef57d4570a1d4

View File

@@ -0,0 +1,46 @@
using UnityEngine;
public class RotateUI : MonoBehaviour
{
[Header("Rotation Settings")]
[SerializeField] private float rotateSpeed = 30f;
[SerializeField] private bool clockwise = true;
[SerializeField] private bool playOnStart = true;
private bool isRotating;
private void Awake()
{
isRotating = playOnStart;
}
private void Update()
{
if (!isRotating)
return;
float dir = clockwise ? -1f : 1f;
transform.Rotate(0f, 0f, dir * rotateSpeed * Time.deltaTime, Space.Self);
}
public void StartRotate()
{
isRotating = true;
}
public void StopRotate()
{
isRotating = false;
}
public void SetClockwise(bool value)
{
clockwise = value;
}
public void ResetRotation()
{
transform.localEulerAngles = Vector3.zero;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6d504793270ff7941b29b9861f3e3944