(7) 상점기능 #4 (아이템 생성방식 변경, 가격태그 추가, 아이템 드래그 구현)
지난 글에서 상점창 알맞는 위치에 각각 조커와 아이템 프리팹을 생성하는데 성공했다 하지만 한가지 문제가 생겼다. 지금은 프리팹을 하나만 만들어 여기서 랜덤으로 아이템 이미지를 가져와
sangeun00.tistory.com
<가격 고유 구현>
이제 상점창에서 아이템 생성, 드래그하는 기능까지 추가해주었고 가격태그도 만들어주었다
그럼 이제 가장 중요한 핵심 기능인데...바로 아이템 기능구현이다
사실상 게임의 재미 대부분이 이 아이템 사용에 달려있기 때문에 가장 중요한 기능이기도 하다
그럼 도대체 이 수많은 아이템들을 어떻게 관리하고 기능구현하는 코드들을 보관해야할까..?
사실 지금도 계속 고민중인 부분이긴 하다
지금 아이템 고유가 가지는 기능은 크게 3가지이다
1. 아이템 이미지
2. 가격
3. 고유 기능
여기서 이미지는 프리팹마다 이미지를 부착해준다니까 그렇다쳐도
가격과 고유기능. 이게 필요한데 만약 아이템이 150개라면 150개의 스크립트를 만들어줄수도 없는 모양
그래서 생각해본 방식으로 먼저 각각 고유한 가격을 가질 수 있도록 해보겠다
public class JokerStat : MonoBehaviour
{
public TextMeshProUGUI pricetag;
public int price;
public string skill;
void Start()
{
pricetag.text = "$" + price.ToString();
}
}
ㅅㄹ홀ㅇㅎ
생각보다 간단하다. 그냥 public으로 스탯을 받게 한뒤 프리팹에 부착해주면
이렇게 Inspecter창에서 변수 Price란에 내가 직접 다르게 써넣을수 있고
void Start()
{
pricetag.text = "$" + price.ToString();
}
가격을 나타내는 텍스트에 내가 써넣은 Price 값을 반영하여 출력하도록 한다
그럼 이렇게 내가 써넣은대로 각각 고유의 가격을 가지게 하는데 성공한다.
<리롤 버튼>
public void OnRerollBtnClick()
{
// 이전에 사용된 조커 프리팹 저장
List<GameObject> previousJokers = new List<GameObject>();
// 현재 ZokerSlot1과 ZokerSlot2의 자식 프리팹 찾기
if (ZokerSlot1 != null && ZokerSlot1.transform.childCount > 0)
{
var child = ZokerSlot1.transform.GetChild(0);
if (child != null)
{
previousJokers.Add(child.gameObject);
instantiatedPrefabs.Remove(child.gameObject);
Destroy(child.gameObject);
}
}
if (ZokerSlot2 != null && ZokerSlot2.transform.childCount > 0)
{
var child = ZokerSlot2.transform.GetChild(0);
if (child != null)
{
previousJokers.Add(child.gameObject);
instantiatedPrefabs.Remove(child.gameObject);
Destroy(child.gameObject);
}
}
// 확률에 따라 프리팹 배열 선택 (조커 80%, 타로 10%, 행성 10%)
GameObject[] GetRandomPrefabArray()
{
float randomValue = Random.Range(0f, 100f);
Debug.Log("randomValue : " + randomValue);
if (randomValue < 80f && ZokerPrefabs.Length > 0)
return ZokerPrefabs;
else if (randomValue < 90f && TaroPrefabs.Length > 0)
return TaroPrefabs;
else if (PlanetPrefabs.Length > 0)
return PlanetPrefabs;
else
return ZokerPrefabs; // 기본값으로 조커 프리팹 반환
}
// 첫 번째 슬롯에 새로운 프리팹 생성
if (ZokerSlot1 != null)
{
// 확률에 따라 프리팹 배열 선택
GameObject[] targetPrefabs = GetRandomPrefabArray();
if (targetPrefabs != null && targetPrefabs.Length > 0)
{
// 사용 가능한 프리팹 필터링
var availablePrefabs = targetPrefabs.Where(p => p != null).ToList();
// 이전에 사용된 프리팹 제외
if (previousJokers.Count > 0)
{
availablePrefabs = availablePrefabs
.Where(p => !previousJokers.Exists(j => j != null && j.name.Replace("(Clone)", "") == p.name))
.ToList();
// 사용 가능한 프리팹이 없으면 필터링 없이 다시 시도
if (availablePrefabs.Count == 0)
availablePrefabs = targetPrefabs.Where(p => p != null).ToList();
}
if (availablePrefabs.Count > 0)
{
// 가중치에 따라 랜덤 선택
int index = Random.Range(0, availablePrefabs.Count);
GameObject selectedPrefab = availablePrefabs[index];
// 프리팹 생성
GameObject instance = Instantiate(selectedPrefab, ZokerSlot1.transform.position, Quaternion.identity, ZokerSlot1.transform);
Vector3 localPos = instance.transform.localPosition;
localPos.z = -1f;
instance.transform.localPosition = localPos;
instantiatedPrefabs.Add(instance);
// 두 번째 슬롯을 위해 첫 번째 슬롯의 프리팹 저장
previousJokers.Add(instance);
}
}
}
리롤은 간단하다.
1. 리롤버튼을 클릭시 조커슬롯 2개에 이전 아이템과 아이템 2개를 새로 생성할것
2. 2개의 아이템은 서로 중복되면 안됨
3. 최대한 랜덤한 아이템이 생성되도록 할것
이 3가지의 조건을 맞춰 코드를 짜준담에 버튼 리스너에 등록해주면 된다
버튼을 누를때마다 아이템이 새로고침되는것이 보인다
자 이제 고유의 가격도 가지고, 드래그도 되니 구매를 하게 해주기만 하면 된다
하지만 여기서 중요한건 구매를 하려면 돈이 필요하다는것. 하지만 아직 게임에서 재화시스템은 구현이 되지 않았다...
심지어 리롤 버튼 구현을 완성하기 위해서도 재화기능은 필수적으로 필요하다. 이거 한번 누르는데 5원이다
그래서 재화 시스템을 먼저 구현하고 조커 구현으로 넘어가보도록 하겠다
<재화 시스템>
오른쪽의 원본 이미지를 온갖 노가다를 해서 바꿔놓았는데 방법을 여기다 적는건 부끄러우니 넘어가도록 하자
본격적으로 재화 시스템을 추가하기 전에 미완성이던 인게임 UI를 완성시켜주었다
어떻게 된게 시스템 개발하는것보다 디자인하는게 훨씬 머리 아프고 시간도 오래걸린다...
자 이제 진짜 재화 시스템을 한번 구현해보도록 해보자
어떻게 구현해야 할지 생각하자니 머리가 깨질것 같은 캐시 아웃창..
사실 다른건 어려운게 없지만 조건에 따라 돈을 받을때도, 못받을때도 있고, 게다가 특정 돈을 주는 조커가 발동되면
추가 변수도 고려해야한다.
그래서 우선은 재화를 얻을수 있는 모든 방법을 정리해주었다
1. 스테이지 클리어 보상
2. 남은 핸드 (각 $1)
3. $5당 이자 1 (최대 5)
4. 조커효과 발동
그리고 게임이 끝나고(진행도중 아님) 캐시아웃창에서 추가 골드를 얻을 수 있게 해주는 조커들
다른것도 몇개 더 있지만 우선 우리가 구현할 건 이 3가지 정도
자 이제 고민해보자...어떻게 하는게 좋을까...
------------------------------------------------------------------
아 좋은 생각이 났다ㅋㅋ
1. 스테이지 클리어 보상 $$$$
2. 남은 핸드 (각 $1) $$
3. $5당 이자 1 (최대 5) $$$$$
4. 조커효과 발동 $$$$
지금 캐시아웃창에 뜰 텍스트를 미리보면 아마 이럴 형식일테고
원작에서는 여기서 단 1골드라도 받는, 해당되는 줄만 차례대로 띄우는 형식이다
하지만 이렇게 할 경우 굉장히 코드가 복잡해지고 그 복잡해지는것에 비해 큰 이점은 없다고 판단되어
1. 스테이지 클리어 보상 $$$$
2. 남은 핸드 (각 $1) -
3. $5당 이자 1 (최대 5) $$$$$
4. 조커효과 발동 -
처음부터 4개를 차례대로 다 띄우되, 해당이 안되는 부분들은 그냥 '-' 슬래시로 공백 처리하겠다
이렇게 해준다면
1. 해당되는 골드 획득 경로 안찾아도 됨
2. 불필요한 오브젝트 움직임 최소화
3. 한눈에 보기 편해짐
4. 굳이 텍스트들을 프리팹화 해주거나 하지 않고 처음부터 제자리에 적어두기만 하면 됨
등의 이점을 오히려 얻을 수 있다
그럼 어느 루트에서 재화를 얻었던간에 무조건 4개의 조건을 전부 보여주고
상황에 따라 공백처리 혹은 얻은 재화를 보여주도록 하겠다
한꺼번에 다같이 구현하려니까 너무 머리가 아파진다. 간단한것부터 구현하자
1. 스테이지 클리어 보상
2. 남은 핸드 (각 $1)
3. $5당 이자 1 (최대 5)
4. 조커효과 발동
돈 우선 제외하고 재화를 얻은 루트부터 순차적으로 활성화 시키는것부터 구현해보겠다
// 캐시 아웃창이 생성되고 나서 얻을 재화의 경로와 재화의 양을 보여주는 스크립트
public class CashOutManager : MonoBehaviour
{
public GameObject CashOutBox;
public GameObject CashOutBtn;
public GameObject BG;
public GameObject ClearTxt;
public GameObject RemainHandTxt;
public GameObject InterestTxt;
public GameObject JokerSkillTxt;
public GameObject ClearM;
public GameObject RemainHandM;
public GameObject InterestM;
public GameObject JokerSkillM;
GameManager gameManager;
private bool hasStartedSequence = false;
void Start()
{
CashOutBtn.SetActive(false);
gameManager = FindAnyObjectByType<GameManager>();
}
IEnumerator ActivateTextsSequentially()
{
// 모든 텍스트를 배열로 관리
GameObject[] textObjects = { ClearTxt, RemainHandTxt, InterestTxt, JokerSkillTxt };
RectTransform bgRect = BG?.GetComponent<RectTransform>();
if (bgRect != null)
{
bgRect.anchoredPosition = new Vector2(bgRect.anchoredPosition.x, 2.7f);
bgRect.sizeDelta = new Vector2(bgRect.sizeDelta.x, 300f);
}
foreach (var textObj in textObjects)
{
if (textObj != null)
{
textObj.SetActive(false);
textObj.transform.localScale = Vector3.zero;
}
}
yield return null;
// 각 텍스트에 대해 순차적으로 애니메이션 적용
for (int i = 0; i < textObjects.Length; i++)
{
var textObj = textObjects[i];
if (textObj == null) continue;
textObj.SetActive(true);
// 커지면서 나타나는 애니메이션
textObj.transform.DOScale(Vector3.one * 1f, 0.2f)
.SetEase(Ease.OutBack);
yield return new WaitForSeconds(0.2f);
// 원래 크기로 돌아오는 애니메이션
textObj.transform.DOScale(Vector3.one, 0.2f);
// BG 애니메이션 (두 번째 텍스트부터 적용)
if (i < textObjects.Length - 1 && bgRect != null)
{
// 다음 BG 위치와 크기 설정
float targetY = 0f;
float targetHeight = 0f;
switch (i + 1) // 다음 인덱스에 해당하는 값으로 설정 PosY 0.4 증가할때 Height은 80만큼 증가가
{
case 1: // 두 번째 텍스트 (첫 번째 인덱스)
targetY = 2.3f;
targetHeight = 378.82f;
break;
case 2: // 세 번째 텍스트
targetY = 1.918f;
targetHeight = 456.219f;
break;
case 3: // 네 번째 텍스트
targetY = 1.41f;
targetHeight = 557.977f;
break;
}
// BG 애니메이션 적용
bgRect.DOAnchorPosY(targetY, 0.3f).SetEase(Ease.OutQuad);
bgRect.DOSizeDelta(new Vector2(bgRect.sizeDelta.x, targetHeight), 0.3f).SetEase(Ease.OutQuad);
}
// 다음 애니메이션까지 대기
yield return new WaitForSeconds(0.5f);
}
CashOutBtn.SetActive(true);
}
void Update()
{
if (CashOutBox != null && CashOutBox.activeInHierarchy && !hasStartedSequence)
{
hasStartedSequence = true;
StartCoroutine(DelayedStartSequence());
}
}
IEnumerator DelayedStartSequence()
{
// 1초 대기 후 시퀀스 시작
yield return new WaitForSeconds(0.7f);
StartCoroutine(ActivateTextsSequentially());
}
}
미리 4개의 재화루트, 4개의 돈이 생성될 장소를 입력받고
첫번째 루트가 DOTween을 통해 부드럽게 활성화 애니메이션이 진행되며
활성화 되는 공간만큼 검정 백그라운드 이미지가 자연스럽게 사이즈를 키우도록 해주었다
무슨 뜻인지 가늠이 안갈수 있으니 영상을 첨부하겠다
아 그리고 코드 중간중간에 CashOutBtn의 기능을 봐서 알겠지만
처음 캐시아웃창이 생성됐을땐 비활성화, 모든 요소가 다 활성화가 되고 나면 그제서야 활성화 되도록 해주었다.
그전까지는 버튼을 누르면 안되니까..
여기까지는 기본적인 기능이었고 이제 실질적인 돈 획득을 구현해줘야 한다
코드 상단에
를 괜히 가져온것이 아니다.
왜냐면 GameManager에 핸드카운트, 현재 돈이 얼마있는지 정보가 여기 담겨있고
이걸 바탕으로 얻을 재화의 양이 정해지기 때문
[Header("캐시아웃에서 받을 돈의 값이 저장되는 변수")]
public int clearreward = 0;
public int remainhand = 0;
public int interestmoney = 0;
public int isjokerskill = 0;
public int totalmoney = 0;
....
void Update()
{
if (CashOutBox != null && CashOutBox.activeInHierarchy && !hasStartedSequence)
{
hasStartedSequence = true;
ShowTotalMoney();
StartCoroutine(DelayedStartSequence());
}
}
public void ShowTotalMoney()
{
if(gameManager.money >= 5 && gameManager.money < 10) interestmoney = 1;
else if(gameManager.money >= 10 && gameManager.money < 15) interestmoney = 2;
else if(gameManager.money >= 15 && gameManager.money < 20) interestmoney = 3;
else if(gameManager.money >= 20 && gameManager.money < 25) interestmoney = 4;
else if(gameManager.money >= 25) interestmoney = 5;
remainhand = gameManager.handcount;
totalmoney = clearreward + remainhand + interestmoney + isjokerskill;
BtnMoneyTxt.GetComponent<TextMeshProUGUI>().text = "$" + totalmoney.ToString();
}
우선 재화를 추가해주는 용 변수를 만들어준뒤
만약 캐시아웃 상자가 활성화된다면 ShowTotalMoney라는 메서드를 실행하게 되는데
여기서 ShowTotalMoney 메서드는 위와 같이 총 이번 라운드에서 받아야할 돈을 산정하는 일명 '정산' 메서드이다
그리고 이 정산금을 모두 합친 totalmoney 변수는 버튼 텍스트와 연결하여 $ 기호와 함께 그대로 뜨도록 해주었다
이제 게임을 클리어하면 자동으로 받아야 할 돈을 정산하고
캐시아웃 버튼에 그대로 총 정산금액을 띄워주게 된다
if(gameManager.money >= 5 && gameManager.money < 10) interestmoney = 1;
else if(gameManager.money >= 10 && gameManager.money < 15) interestmoney = 2;
else if(gameManager.money >= 15 && gameManager.money < 20) interestmoney = 3;
else if(gameManager.money >= 20 && gameManager.money < 25) interestmoney = 4;
else if(gameManager.money >= 25) interestmoney = 5;
그렇게 잘 구현이 된 것 같은데 한가지 거슬리는것...
코드가 너무 단순한데다 후에 이자로 받을 수 있는 돈의 한계를 올려주는 아이템이 있는데
이렇게 한다면 무조건 이자의 끝은 5로 고정이 되어있다는것
[Header("이자 관련 설정")]
[Tooltip("이자 한계값 (기본값: 5, money 25 이상일 때 최대치)")]
public int interestMaxLimit = 5;
[Tooltip("이자 증가 단위 (money 5당 1 증가)")]
public int interestIncrementUnit = 5;
...
// money를 단위로 나누어 interest 계산 (5당 1 증가, 최대 interestMaxLimit)
interestmoney = Mathf.Min(
gameManager.money / interestIncrementUnit, // money를 단위로 나눈 값
interestMaxLimit // 최대값 제한
);
// 0 이하인 경우 0으로 설정 (옵션: 필요에 따라 제거 가능)
interestmoney = Mathf.Max(0, interestmoney);
그래서 훗날 아이템을 구현할때 수정이 쉽도록 이자 한계값과 이자 증가단위까지 변수 두개로 만들어
손쉽게 조절할 수 있도록 바꿔주었다.
중간에 한가지 팁!
변수 앞에 Tooltip 키워드로 설명문을 붙여넣어준다면
이렇게 Inspector 창에서 변수 위에 마우스를 올려두고 있으면 Tooltip에 써둔 설명문이 나온다
이미 Header 기능이 있긴 하지만 변수 하나하나마다 헷갈리는 기능들이라면 이렇게 써두는것도 참 좋을것 같다
자 이제 정말 다 되었다. 유일한 빈칸인 달러싸인이 획득한 재화값만큼 생성되는 기능만 구현해주면 된다
생성되는것은 딱 두가지다
달러싸인과 공백 표시
두개를 프리팹화하여 저장해주었다
그런 다음 스크립트에서 받아주고
// 돈 아이콘을 생성하는 코루틴
private IEnumerator CreateMoneyIconsCoroutine(GameObject parent, int count, bool isNegative = false)
{
if (parent == null) yield break;
// 기존에 생성된 아이콘 삭제
foreach (Transform child in parent.transform)
{
Destroy(child.gameObject);
}
// 0 이하인 경우 마이너스 아이콘 생성
if (count <= 0)
{
if (MinusPrefab != null)
{
var minusIcon = Instantiate(MinusPrefab, parent.transform, false);
// 원래 스케일 저장
Vector3 minusOriginalScale = minusIcon.transform.localScale;
// 마이너스 아이콘도 부드럽게 나타나도록 애니메이션 적용
minusIcon.transform.localScale = Vector3.zero;
minusIcon.transform.DOScale(minusOriginalScale * 1.2f, 0.2f).SetEase(Ease.OutBack);
minusIcon.transform.DOScale(minusOriginalScale, 0.1f).SetDelay(0.2f);
}
yield break;
}
// 양수인 경우 달러 아이콘 생성
if (DollarPrefab != null)
{
for (int i = 0; i < count; i++)
{
float xOffset = -0.333f * i;
Vector3 position = new Vector3(xOffset, 0, 0);
var instance = Instantiate(DollarPrefab, parent.transform, false);
instance.transform.localPosition = position;
Vector3 originalScale = instance.transform.localScale;
instance.transform.localScale = Vector3.zero;
instance.transform.DOLocalMoveY(0.2f, 0.15f).From().SetEase(Ease.OutQuad);
instance.transform.DOScale(originalScale * 1.3f, 0.2f)
.SetEase(Ease.OutBack)
.OnComplete(() => {
instance.transform.DOScale(originalScale, 0.1f);
});
float randomRot = Random.Range(-5f, 5f);
instance.transform.DORotate(new Vector3(0, 0, randomRot), 0.3f).SetEase(Ease.OutElastic);
yield return new WaitForSeconds(0.07f);
}
}
}
상황에 따라 달러싸인 혹은 공백으로 처리하는 프리팹을 생성하도록 하였고
이것 역시 DOTween으로 부드럽고 이쁘게 생성되는 효과를 추가하였다
public void OnCashOutButton()
{
gameManager.money += cashOutManager.totalmoney;
gameManager.UpdateUI();
StartCoroutine(HideCashOutAndShowShop());
}
그리고 캐시아웃 버튼을 눌렀을때 totalmoney를 현재 재화에 추가하도록 하였다.
이제 정말 돈이라는것을 먹을수 있게 되었다ㅠㅠ
26원이었던 돈이 8원을 먹음으로써 34원이 되는것을 확인할 수 있다
무려 5시간에 걸친 결과물이 나오게 되었다
감격스럽다 흑흑
'Galaxy Card > 1-1. 시스템 개발 #인게임' 카테고리의 다른 글
(10) 상점기능#7 (상점기능 보완) (6) | 2025.06.26 |
---|---|
(9) 상점기능#6 (아이템 구매처리 구현) (2) | 2025.06.08 |
(7) 상점기능 #4 (아이템 생성방식 변경, 가격태그 추가, 아이템 드래그 구현) (1) | 2025.05.29 |
(6) 상점기능#3 (플레이존 Collider 수정, 아이템 생성) (2) | 2025.05.27 |
(5) 상점기능#2 (상점창 제작, 상점창 움직임 구현) (3) | 2025.05.21 |