강화 시스템
아이템 타입과 강화 단계에 따라 필요한 재화와 재료를 계산하고, 룰렛을 이용한 확률 기반 강화 결과를 제공한다.
성공과 실패는 명확히 분기되며, 각 결과에 따라 아이템 상태와 시각·청각적 피드백이 즉시 반영된다.
전체 플레이 영상
구현 내용
전체 코드
using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class UpgradeSlot : MonoBehaviour
{
private const string CrystalItemId = "200_02_01"; // 강화 크리스탈 아이템 ID
ItemManager itemManager;
Item selectedItem; // 선택한 아이템의 대한 정보
[Header("선택된 아이템 정보")]
[SerializeField] Image icon = null; // 아이템 이미지
[SerializeField] Image gradeImage = null; // 아이템의 강화단계 이미지
[SerializeField] TextMeshProUGUI itemName = null; // 아이템 이름
[SerializeField] TextMeshProUGUI upgradeInfo = null; // 강화 정보 메세지 (현재 레벨 > 다음 레벨)
[Header("필요한 아이템 정보")]
[SerializeField] TextMeshProUGUI crystalCount = null; // '소지하고 있는 수량 / 필요한 크리스탈 수량'
[SerializeField] TextMeshProUGUI reqCoin = null; // 필요한 재화
int requiredCoin; // 필요한 재화
int requiredCrystal; // 필요한 크리스탈 수량
int currentCrystalCount; // 소유하고 있는 크리스탈 수량
[SerializeField] GameObject upgradeBtn; // 강화 버튼
[SerializeField] GameObject warningText; // 재료가 부족할 때 띄울 경고 메세지
[Header("룰렛")]
public GameObject roulettePanel; // 룰렛 패널
[HideInInspector] public bool isRotating = false; // 룰렛 회전 여부
[HideInInspector] public bool upgradeAttempted = false; // 강화의 추가 시도를 제한
[Header("결과창")]
public GameObject resultPanel; // 결과 패널
public TextMeshProUGUI percentTxt; // 강화 확률
public TextMeshProUGUI resultTxt; // 강화 결과
[Header("이펙트 & 사운드")]
[SerializeField] private ParticleSystem successEffect; // 성공 이펙트
[SerializeField] private ParticleSystem failEffect; // 실패 이펙트
[SerializeField] private AudioSource audioSource; // 공용 오디오 소스
[SerializeField] private AudioClip successClip; // 성공 사운드
[SerializeField] private AudioClip failClip; // 실패 사운드
void Start()
{
itemManager = GameManager_LDW.instance.itemManager;
CancelSelection();
}
void Update()
{
currentCrystalCount = itemManager.GetItem(CrystalItemId).count; // 현재 소지하고 있는 크리스탈 수량
// [ 선택한 아이템에 따라 재료 설정 ]
// 아이템 선택 시
if (selectedItem != null)
{
UpdateSelectedItemUI();
UpdateRequirementsByType();
UpdateUpgradeState();
}
UpdateGradeImageVisibility();
}
public void UpdateItem()
{
currentCrystalCount = itemManager.GetItem(CrystalItemId).count; // 현재 소지하고 있는 크리스탈 수량
}
// 아이템 선택
public void SelectItem(Item _item) // SelectItem함수에서 Item을 받아옴
{
selectedItem = _item; // SelectedItem에 받아온 아이템의 정보를 담음
}
// 선택된 아이템의 기본 UI 정보 업데이트
private void UpdateSelectedItemUI()
{
icon.enabled = true; // 선택된 아이템이 있다면, 아이템 아이콘 이미지를 활성화
icon.sprite = selectedItem.itemImage; // icon에 선택한 아이템의 이미지를 가져옴
gradeImage.sprite = selectedItem.gradeSprite; // 강화 정보에 선택한 아이템의 강화 이미지를 가져옴
itemName.text = selectedItem.itemName; // 선택한 아이템 이름을 화면에 띄움
}
// 아이템 선택 취소 및 초기화 (UI 리셋)
public void CancelSelection()
{
// 선택된 아이템 정보 초기화
selectedItem = null;
// 아이콘 / 강화 이미지 / 텍스트들 리셋
icon.enabled = false;
icon.sprite = null;
gradeImage.enabled = false;
gradeImage.sprite = null;
itemName.text = "";
upgradeInfo.text = "";
crystalCount.text = "";
reqCoin.text = "";
// 강화 버튼 / 경고 문구 비활성화
BtnOrErr(false, false);
// 룰렛 / 결과창도 혹시 열려 있으면 닫기
if (roulettePanel != null) roulettePanel.SetActive(false);
if (resultPanel != null) resultPanel.SetActive(false);
// 룰렛 상태 플래그 초기화
isRotating = false;
upgradeAttempted = false;
}
// 강화 단계 이미지 표시 여부
private void UpdateGradeImageVisibility()
{
//이미 강화된 아이템이면, 강화 단계 이미지를 활성화, 강회되지 않았으면 강화 단계 이미지 비활성화
gradeImage.enabled = (gradeImage.sprite != null);
}
// --- 비용 계산 / 조건 설정 --- //
// 아이템 타입에 따라 강화 설정 분기
private void UpdateRequirementsByType()
{
if (selectedItem.equipType == Item.EquipType.Weapon)
{
SetAccordingToType(1, 10); // 무기: x = 1, 최대강화 10
}
else
{
SetAccordingToType(2, 3); // 예: 반지: x = 2, 최대강화 3
}
}
// 아이템 타입에 따른 필요 재료 및 UI 텍스트 설정
private void SetAccordingToType(int x, int maxGrade)
{
requiredCoin = (selectedItem.grade + 1) * 400 * x; // 필요한 재화 = (현재 강화 단계 + 1) * 400 * x
requiredCrystal = (selectedItem.grade + 1) * x; // 필요한 크리스탈 = 현재 강화 단계 + 1 * x
if (selectedItem.grade < maxGrade) // 최대 강화가 아닐 때 (최대강화 단계 = 10)
{
// 강화 정보 UI 업데이트
upgradeInfo.text = $"{selectedItem.grade} > {selectedItem.grade + 1}";
crystalCount.text = $"{currentCrystalCount} / {requiredCrystal}";
reqCoin.text = $"필요 재화: {requiredCoin}";
}
else // 최대 강화
{
upgradeInfo.text = "최대 강화";
crystalCount.text = "-";
reqCoin.text = "필요 재화: -";
}
}
// 강화가 가능 여부에 따라 버튼/경고 상태 제어
private void UpdateUpgradeState()
{
// 해당 아이템이 이미 최대 강화라면
if (upgradeInfo.text == "최대 강화")
{
BtnOrErr(false, false);
return;
}
bool hasEnoughCrystal = currentCrystalCount >= requiredCrystal;
bool hasEnoughCoin = itemManager.coin >= requiredCoin;
// 강화 가능
if (hasEnoughCrystal && hasEnoughCoin) BtnOrErr(true, false);
// 재료 부족
else BtnOrErr(false, true);
}
// 강화버튼 / 경고 활성화 상태 관리
public void BtnOrErr(bool btn, bool err)
{
upgradeBtn.SetActive(btn);
warningText.SetActive(err);
}
/// --- 강화 시작 / 룰렛 --- //
// 강화 클릭 시 호출
public void UpgradeBtn()
{
// 재화, 크리스탈 차감
itemManager.coin -= requiredCoin;
itemManager.GetItem(CrystalItemId).count -= requiredCrystal;
// 룰렛 창 활성화
roulettePanel.SetActive(true);
// 룰렛 회전 시작
upgradeAttempted = false; // 강화의 추가 시도를 가능 (룰렛 창에서 멈춤 버튼을 안눌렀음)
isRotating = true; // 룰렛 변수를 true로 하여, 룰렛이 돌아가도록 설정
}
// 멈추기 버튼 클릭 시 호출
public void StopBtn()
{
isRotating = false; // 룰렛을 멈춤
}
/// --- 강화 결과 처리 --- ///
// 룰렛이 멈추었을 때, 해당 확률로 강화 시도
public void AttemptUpgradeWithProbability(string probabilityText)
{
// 룰렛/결과 패널 잠깐 보여주기
StartCoroutine(ShowTemp(roulettePanel, 1f)); // 1초 뒤에 upgradeSystemPanel을 비활성화 함
StartCoroutine(ShowTemp(resultPanel, 1f)); // 결과 창인 resultPanel을 활성화하고, 0.5초 뒤에 비활성화 함
// 이미 한 번 처리된 강화라면 중복 실행 방지
if (upgradeAttempted) return;
// 확률 문자열을 실수로 변환
if (!float.TryParse(probabilityText, out float successProbability))
return;
// 0-100 사이의 확률 값을 0-1 사이로 변환함
successProbability /= 100f;
percentTxt.text = $"강화 확률 : {probabilityText}%"; // 강화 확률 텍스트에 '강화 확률 : 현재 강화 확률 %'로 초기화
bool isSuccess = Random.value <= successProbability;
ApplyUpgradeResult(isSuccess);
upgradeAttempted = true; // 성공 또는 실패 후에 upgradeAttempted를 true로 설정하여 추가 시도 제한
}
// 성공/실패에 따른 결과 텍스트 및 강화 단계 반영
private void ApplyUpgradeResult(bool success)
{
if (success)
{
int currentGrade = selectedItem.grade;
int nextGrade = currentGrade + 1;
selectedItem.grade = nextGrade;
resultTxt.color = Color.yellow;
resultTxt.text = $"강화 성공: {currentGrade} -> {nextGrade}";
UpdateSelectedItemUI();
UpdateRequirementsByType();
UpdateUpgradeState();
PlayResultEffect(true);
}
else
{
resultTxt.color = Color.red;
resultTxt.text = "강화 실패";
PlayResultEffect(false);
}
}
// obj 오브젝트를 duration만큼만 활성화
private IEnumerator ShowTemp(GameObject obj, float duration)
{
obj.SetActive(true);
yield return new WaitForSeconds(duration);
obj.SetActive(false);
}
// 성공/실패 공통 이펙트 재생
private void PlayResultEffect(bool success)
{
// 파티클
if (success && successEffect != null)
successEffect.Play();
else if (!success && failEffect != null)
failEffect.Play();
// 사운드
if (audioSource != null)
{
AudioClip clip = success ? successClip : failClip;
if (clip != null)
audioSource.PlayOneShot(clip);
}
}
}using TMPro;
using UnityEngine;
public class RouletteCollisionDetector : MonoBehaviour
{
private UpgradeSlot upgrade;
private void Start()
{
// UpgradeSlot 컴포넌트가 현재 GameObject에 없으면 부모에서 찾음
upgrade = GetComponentInParent<UpgradeSlot>();
}
void OnTriggerStay(Collider other)
{
// upgrade가 null이 아니고, 부딪힌 객체의 태그가 Percent이고, isRotating이 false일 때만 충돌을 체크.
if (!upgrade.isRotating && other.CompareTag("Percent") && !upgrade.upgradeAttempted)
{
Debug.Log("충돌!");
TextMeshProUGUI probabilityText = other.GetComponentInChildren<TextMeshProUGUI>(); // 부딪힌 객체의 TextMeshProUGUI 컴포넌트를 찾아서 참조
if (probabilityText != null)
{
Debug.Log("확률" + probabilityText.text);
upgrade.AttemptUpgradeWithProbability(probabilityText.text);
}
}
}
}using UnityEngine;
public class Roulette : MonoBehaviour
{
private UpgradeSlot upgrade;
RectTransform rtf;
private float rotationSpeed = 1000.0f; // 룰렛이 돌아가는 속도
private void Start()
{
upgrade = GetComponentInParent<UpgradeSlot>();
rtf = GetComponent<RectTransform>();
}
private void Update()
{
if (upgrade.isRotating)
{
rtf.Rotate(0f, 0f, rotationSpeed); // 룰렛의 rectTransform에서 rotation에서 z축만 돌림
}
}
}전체 흐름도





