๐ŸŽก Unity ๋ฃฐ๋ › ๋ฝ‘๊ธฐ ์‹œ์Šคํ…œ ๊ตฌํ˜„ํ•˜๊ธฐ (ScriptableObject + UI)

๊น€๋ณด๊ทผยท2025๋…„ 7์›” 7์ผ

Unity

๋ชฉ๋ก ๋ณด๊ธฐ
110/113

๐ŸŽก Unity ๋ฃฐ๋ › ๋ฝ‘๊ธฐ ์‹œ์Šคํ…œ ๊ตฌํ˜„ํ•˜๊ธฐ (ScriptableObject + UI)

์˜ค๋Š˜์€ Unity์—์„œ ๋ฃฐ๋ › ํ˜•์‹์˜ ๋ฝ‘๊ธฐ ์‹œ์Šคํ…œ์„ ์ง์ ‘ ๊ตฌํ˜„ํ•ด๋ดค๋‹ค.
์•„์ดํ…œ ๋ฐ์ดํ„ฐ๋Š” ScriptableObject๋กœ ๊ด€๋ฆฌํ•˜๊ณ , ๋ฃฐ๋ › ์นธ์€ UI๋กœ ์ž๋™ ์ƒ์„ฑ๋˜๋„๋ก ํ–ˆ๋‹ค.

โœ… ๊ตฌํ˜„ ๋ชฉํ‘œ

  • ๋ฃฐ๋ › ์›ํŒ ์œ„์— ์•„์ดํ…œ ์ •๋ณด๊ฐ€ UI๋กœ ํ‘œ์‹œ๋˜๊ณ 

  • ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ๋ฃฐ๋ ›์ด ํšŒ์ „ํ•˜๋ฉด์„œ ์•„์ดํ…œ์„ ๋žœ๋ค์œผ๋กœ ๋ฝ‘๊ณ 

  • ๊ฐ ์•„์ดํ…œ์€ ์ง€์ •ํ•œ ํ™•๋ฅ ์— ๋”ฐ๋ผ ๋ฝ‘ํžˆ๋„๋ก ๊ตฌ์„ฑ

๐Ÿงฉ ๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ โ€“ ScriptableObject

์•„์ดํ…œ ์ •๋ณด๋Š” GachaItemData๋ผ๋Š” ScriptableObject๋กœ ๋งŒ๋“ค๊ณ ,
์ด ์•ˆ์— ์ด๋ฆ„, ํ™•๋ฅ , ์•„์ด์ฝ˜, ์„ค๋ช…์„ ์ €์žฅํ–ˆ๋‹ค.

[CreateAssetMenu(fileName = "NewGachaItem", menuName = "Gacha/Item")]
public class GachaItemData : ScriptableObject
{
    public string itemName;
    public float probability;
    public Sprite icon;
    [TextArea] public string description;
}

๊ทธ๋ฆฌ๊ณ  GachaDatabase ๋ผ๋Š” ScriptableObject์—์„œ
์ด ์•„์ดํ…œ๋“ค์„ ๋ฐฐ์—ด๋กœ ๋ชจ์•„์„œ ๊ด€๋ฆฌํ–ˆ๋‹ค.

๐ŸŒ€ ๋ฃฐ๋ › UI ๋ฐฐ์น˜ โ€“ GachaWheelUI.cs

GachaWheelUI ์Šคํฌ๋ฆฝํŠธ์—์„œ๋Š” ์›ํŒ ์œ„์— ๊ฐ ์•„์ดํ…œ ์ •๋ณด๋ฅผ ๋‹ด์€ UI(Sector)๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•œ๋‹ค.

void CreateWheelSectors()
{
    int itemCount = gachaDatabase.items.Length;
    float segmentAngle = 360f / itemCount;

    for (int i = 0; i < itemCount; i++)
    {
        var item = gachaDatabase.items[i];
        GameObject sector = Instantiate(sectorPrefab, wheelParent);

        RectTransform rt = sector.GetComponent<RectTransform>();
        rt.anchorMin = rt.anchorMax = new Vector2(0.5f, 0.5f);
        rt.pivot = new Vector2(0.5f, 0f); // ์ค‘์‹ฌ ์•„๋ž˜์ชฝ
        rt.localPosition = Vector3.zero;
        rt.localScale = Vector3.one;

        float angle = -segmentAngle * i - (segmentAngle / 2f);
        rt.localRotation = Quaternion.Euler(0, 0, angle);

        // UI ์„ค์ •
        rt.Find("Icon").GetComponent<Image>().sprite = item.icon;
        rt.Find("NameText").GetComponent<TextMeshProUGUI>().text = item.itemName;
        rt.Find("ProbabilityText").GetComponent<TextMeshProUGUI>().text = $"{item.probability}%";
        rt.Find("DescriptionText").GetComponent<TextMeshProUGUI>().text = item.description;
    }
}

์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•œ ๊ฑด RectTransform์˜ Pivot๊ณผ Anchor ์„ค์ •์ด๋‹ค.
(0.5, 0)๋กœ ์„ค์ •ํ•˜๋ฉด ์ค‘์‹ฌ ๊ธฐ์ค€์œผ๋กœ ๋ฐ”๊นฅ์ชฝ์œผ๋กœ ํŽผ์ณ์ง€๋“ฏ ๋ฐฐ์น˜๋œ๋‹ค.

๐ŸŽฏ ๋ฃฐ๋ › ํšŒ์ „ ๋กœ์ง โ€“ GachaWheel.cs

๋ฃฐ๋ › ํšŒ์ „์€ GachaWheel ์Šคํฌ๋ฆฝํŠธ์—์„œ ์ฒ˜๋ฆฌํ–ˆ๋‹ค.
๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ํ™•๋ฅ ์— ๋”ฐ๋ผ ์•„์ดํ…œ์„ ํ•˜๋‚˜ ์„ ํƒํ•˜๊ณ ,
ํ•ด๋‹น ์นธ ๋ฒ”์œ„์— ๋งž์ถฐ ๋ฃฐ๋ ›์„ ๋Œ๋ฆฐ๋‹ค.

IEnumerator Spin()
{
    isSpinning = true;

    GachaItemData selectedItem = GetRandomItem();
    int itemCount = gachaDatabase.items.Length;
    int index = System.Array.IndexOf(gachaDatabase.items, selectedItem);

    float segmentAngle = 360f / itemCount;
    float minAngle = index * segmentAngle;
    float maxAngle = minAngle + segmentAngle;
    float targetAngle = Random.Range(minAngle, maxAngle);

    float startAngle = wheel.eulerAngles.z;
    float endAngle = 360f * Random.Range(5, 7) + targetAngle;
    float duration = 4f;
    float elapsed = 0f;

    while (elapsed < duration)
    {
        elapsed += Time.deltaTime;
        float t = elapsed / duration;
        float eased = Mathf.SmoothStep(0, 1, t);
        float angle = Mathf.Lerp(startAngle, endAngle, eased);
        wheel.eulerAngles = new Vector3(0, 0, angle);
        yield return null;
    }

    Debug.Log($"ํš๋“ ์•„์ดํ…œ: {selectedItem.itemName}");
    isSpinning = false;
}

์ด๋•Œ ๋ฃฐ๋ ›์€ 5~7๋ฐ”ํ€ด + ํ•ด๋‹น ์•„์ดํ…œ ๊ฐ๋„ ๋งŒํผ ํšŒ์ „ํ•ด์„œ
์‹œ๊ฐ์ ์œผ๋กœ ๊ทธ๋Ÿด๋“ฏํ•˜๊ฒŒ ๋Œ์•„๊ฐ„๋‹ค.
SmoothStep์„ ์จ์„œ ํšŒ์ „์ด ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๊ฐ์†๋˜๋ฉฐ ๋ฉˆ์ถ˜๋‹ค.

๐ŸŽฒ ํ™•๋ฅ ์— ๋”ฐ๋ฅธ ์•„์ดํ…œ ๋ฝ‘๊ธฐ

ํ™•๋ฅ ์€ probability ํ•„๋“œ์— ๋”ฐ๋ผ ๊ฐ€์ค‘์น˜ ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ํ–ˆ๋‹ค.

GachaItemData GetRandomItem()
{
    var items = gachaDatabase.items;
    float total = items.Sum(i => i.probability);
    float rand = Random.Range(0, total);
    float sum = 0;

    foreach (var item in items)
    {
        sum += item.probability;
        if (rand <= sum)
            return item;
    }

    return items.Last(); // fallback
}

๐Ÿ“Œ ๊ตฌํ˜„ํ•˜๋ฉด์„œ ์•Œ๊ฒŒ๋œ ์ 

  • RectTransform์—์„œ Pivot, Anchor, Rotation์ด UI ๋ฐฐ์น˜์— ๋งค์šฐ ์ค‘์š”ํ•˜๋‹ค

  • ๋ฃฐ๋ ›์ฒ˜๋Ÿผ ํšŒ์ „ ๊ธฐ๋ฐ˜ UI๋Š” Sector ์ค‘์‹ฌ์ด ์ •ํ™•ํžˆ ๋ฐ”๋Š˜๊ณผ ๋งž์•„์•ผ ์‹œ๊ฐ์ ์œผ๋กœ ์–ด์ƒ‰ํ•˜์ง€ ์•Š์Œ

  • ScriptableObject๋ฅผ ํ™œ์šฉํ•˜๋ฉด ์•„์ดํ…œ ์ถ”๊ฐ€/์ˆ˜์ •์ด ๋งค์šฐ ์œ ์—ฐํ•ด์ง„๋‹ค

  • ํ™•๋ฅ ํ˜• ๋ฝ‘๊ธฐ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค ๋•Œ๋Š” ๋‹จ์ˆœํ•œ ๋žœ๋ค์ด ์•„๋‹Œ ํ™•๋ฅ  ๊ฐ€์ค‘์น˜ ๋กœ์ง์ด ํ•„์š”ํ•˜๋‹ค

๐Ÿ’ก ๋‹ค์Œ์— ํ•ด๋ณด๊ณ  ์‹ถ์€ ๊ฐœ์„ 

  • ์•„์ดํ…œ ๋“ฑ๊ธ‰์— ๋”ฐ๋ผ ์‹œ๊ฐ์  ํšจ๊ณผ ๋‹ค๋ฅด๊ฒŒ ์ฃผ๊ธฐ (์—ํ”ฝ์€ ๋ฒˆ์ฉ ๋ฒˆ์ฉ โœจ)

  • ๋ฝ‘๊ธฐ ๊ฒฐ๊ณผ ํŒ์—…์ฐฝ ์ถ”๊ฐ€ (Get!)

  • ์—ฌ๋Ÿฌ ๊ฐœ ๋™์‹œ์— ๋ฝ‘๋Š” 10์—ฐ ๋ฝ‘๊ธฐ ์‹œ์Šคํ…œ

  • ๋ฃฐ๋ › ์• ๋‹ˆ๋ฉ”์ด์…˜ ์—ฐ์ถœ ๋” ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ (์‚ฌ์šด๋“œ, ๋–จ๋ฆผ ๋“ฑ)

๐Ÿ”š ๋งˆ๋ฌด๋ฆฌ

์ด๋ฒˆ ์ž‘์—…์„ ํ†ตํ•ด UI ๋ฐฐ์น˜์™€ ์• ๋‹ˆ๋ฉ”์ด์…˜, ScriptableObject์˜ ์œ ์šฉํ•จ์„ ๋‹ค์‹œ ํ•œ๋ฒˆ ์ฒด๊ฐํ–ˆ๋‹ค.
๋ฃฐ๋ › UI๋ฅผ ์ง์ ‘ ๊ตฌ์„ฑํ•˜๋ฉด์„œ ๊ฐ๋„ ๊ณ„์‚ฐ๊ณผ UI ์ •๋ ฌ์ด ์–ผ๋งˆ๋‚˜ ์ค‘์š”ํ•œ์ง€๋„ ์•Œ๊ฒŒ ๋๋‹ค.

profile
๊ฒŒ์ž„๊ฐœ๋ฐœ์ž๊ฟˆ๋‚˜๋ฌด

0๊ฐœ์˜ ๋Œ“๊ธ€