플레이어 스탯 시스템

플레이어 스탯 시스템은 캐릭터의 능력치를 관리하고 성장 상태를 시각적으로 표현하는 시스템이다.

플레이어는 체력(HP), 배터리(BT), 이동속도(SP), 공격력(ATK), 채집속도(GSP)와 같은 능력치를 가지며 게임 진행에 따라 스탯을 강화할 수 있다.

목차

1. 유니티 구현

     1.1. ScripatableObject 기반 스탯 데이터

    1.2. NGUI 기반 UILabel

    1.3. NGUI 기반 UISprite

    1.4. NGUI 기반 Grid

2. 전체 코드

유니티 구현

1.1. ScriptableOjbect 기반 스탯 데이터

플레이어의 능력치 데이터는 ScriptableObject를 이용하여 관리하였다.

ScriptableObject는 Unity에서 데이터를 독립적인 에셋 형태로 관리할 수 있도록 하는 데이터 구조이다.

이를 활용하면 스크립트 내부에 데이터를 직접 작성하는 방식과 달리, 데이터와 로직을 분리하여 관리할 수 있다.

PlayerStatus ScriptableObject에는 다음과 같은 플레이어 스탯 정보가 저장된다.

- HP : 플레이어 체력
- BT : 플레이어 배터리
- SP : 플레이어 이동속도
- ATK : 플레이어 공격력
- GSP : 플레이어 채집속도

또한, 각 능력치의 초기값과 강화 증가량, 스탯 포인트 상태 등을 함께 관리하도록 구성하였다.

ScriptableObject를 사용함으로써 플레이어 스탯 데이터를 에셋 단위로 관리할 수 있으며, 코드 수정 없이 인스펙터에서 능력치 밸런싱을 조정할 수 있다.

또한 데이터와 로직을 분리하여 시스템 구조를 명확하게 유지할 수 있다.

이러한 구조를 통해 플레이어 능력치 시스템을 유연하게 관리할 수 있도록 설계하였다.

1.2. NGUI 기반 UILabel

플레이어의 현재 능력치와 강화 상태를 확인할 수 있도록 NGUI 기반 UI 시스템을 구현하였다.

스탯 UI에서는 각 능력치의 현재 값과 강화 상태를 텍스트(UILabel)와 이미지(UISprite)를 통해 표현한다.

먼저, UILabel은 텍스트 정보를 표시하는 UI 컴포넌트이다.

UILabel을 이용하여 현재 스탯 값, 스탯 강화 퍼센트, 총 강화 수치를 표현하였다.

UILabel을 통해 플레이어가 자신의 능력치 상태를 수치적으로 확인할 수 있도록 구성하였다.

또한 HUD에서는 체력과 배터리의 현재 값을 텍스트 형태로 함께 표시하여 플레이어가 자신의 상태를 직관적으로 파악할 수 있도록 하였다.

1.3. NGUI 기반 UISprite

UISprite는 이미지 기반 UI를 표현하는 컴포넌트이다.

UISprite를 이용하여 HUD에서는 체력바와 배터리바, 스탯 UI에서는 전체 성장도 원형 그래프와 스탯 강화 바를 표현하였다.

UISprite에서 Type을 Filled로 설정하고, Flip은 Nothing, Fill Dir을 Horizontal로 설정하면 가로로 채워지는 바 형태를 만들 수 있다.

여기서 Fill Dir을 'Radial 360'으로 설정하면 원형 모양으로 채워지는 그래프 형태를 만들 수 있다.

UISprite의 fillAmount 값을 이용하여 바와 원형 그래프를 실시간으로 표현하도록 구성하였다.

이를 통해 플레이어는 자신의 현재 상태를 직관적으로 확인할 수 있다.

1.4. NGUI 기반 Grid

스탯 UI에서는 여러 스탯의 UILabel을 정렬하기 위해 NGUI Grid 컴포넌트를 사용하였다.

Grid는 UI 오브젝트들을 일정한 규칙에 따라 자동으로 정렬해주는 레이아웃 컴포넌트이다.

Grid에서는 다음과 같은 주요 설정을 통해 UI 정렬을 구성하였다.

- Arrangement : UI 요소 정렬 방향 설정
- Cell Width / Cell Height : 각 UI 요소의 크기 설정
- Row Limit : 한 줄에 배치되는 UI 요소 수 설정

Grid를 이용하여 스탯 UI의 요소들이 일정한 간격과 정렬 구조를 유지하도록 구성하였다.

전체 코드

using UnityEngine;

[CreateAssetMenu(fileName = "PlayerStatus", menuName = "Player/Status")]
public class PlayerStatus : ScriptableObject
{
    public const int maxPoint = 5;

    [SerializeField] float initHp;      // 초기 체력
    [SerializeField] float initBt;     // 초기 배터리
    [SerializeField] float initSp;        // 초기 이동속도
    [SerializeField] float initAtk;     // 초기 힘
    [SerializeField] float initGsp;      // 초기 채집속도

    [SerializeField] int hpPoint;
    [SerializeField] int btPoint;
    [SerializeField] int spPoint;
    [SerializeField] int atkPoint;
    [SerializeField] int gspPoint;

    [SerializeField] float hpIncrease;
    [SerializeField] float btIncrease;
    [SerializeField] float spIncrease;
    [SerializeField] float atkIncrease;
    [SerializeField] float gspIncrease;

    [SerializeField] bool isForestUnLock;
    [SerializeField] bool isDesertUnLock;
    [SerializeField] bool isCaveUnLock;

    public int MAXPOINT
    {
        get { return maxPoint; }
    }

    #region 스탯 프로퍼티
    public float HP
    {
        get
        {
            return initHp + hpPoint * hpIncrease;
        }
    }

    public float BT
    {
        get
        {
            return initBt + btPoint * btIncrease;
        }
    }

    public float SP
    {
        get
        {
            return initSp + spPoint * spIncrease;
        }
    }

    public float ATK
    {
        get
        {
            return initAtk + atkPoint * atkIncrease;
        }
    }

    public float GSP
    {
        get
        {
            return initGsp - gspPoint * gspIncrease;
        }
    }
    #endregion

    #region 스탯 레벨 포인트 프로퍼티
    public int HpPoint { get; set; }
    public int BtPoint { get; set; }
    public int SpPoint { get; set; }
    public int AtkPoint { get; set; }
    public int GspPoint { get; set; }
    #endregion

    #region 스킬 포인트당 증가량 프로퍼티
    public float HpIncrease { get; }
    public float BtIncrease { get; }
    public float SpIncrease { get; }
    public float AtkIncrease { get; set; }
    public float GspIncrease { get; }
    #endregion

    #region 맵 해금 상태
    // 숲 해금 상태
    public bool IsForestUnLock { get; set; }

    // 사막 해금 상태
    public bool IsDesertUnLock { get; set; }

    // 동굴 해금 상태
    public bool IsCaveUnLock { get; set; }
    #endregion
}
using System.Collections;
using UnityEngine;

public class StatusManager : MonoBehaviour
{
    public static StatusManager instance;   

    [SerializeField] PlayerStatus playerStatus;  
    [SerializeField] float currentHp;    // 현재 체력
    [SerializeField] float currentBt;    // 현재 배터리

    // 체력 감소, 배터리 증가&감소 관련 로직 => 무한 반복되지 않도록
    bool plusBattery = false;
    bool minusBattery = false;
    bool minusHp = false;
    bool minusHpMap = false;

    // 플레이어가 죽은 상태인지 확인
    public bool die = false;

    // 치트키 관련 변수
    bool isCheatHp;
    bool isCheatBt;
    bool isCheatAtk;
    float befAtk;

    #region Property
    public PlayerStatus PlayerStatus
    {
        get { return playerStatus; }
        set { playerStatus = value; }
    }

    // 최대 체력
    public float MaxHp
    {
        get { return playerStatus.HP; }
    }

    // 현재 체력
    public float CurrentHp
    {
        get { return currentHp; }
        set
        {
            currentHp = value;
            if(currentHp > MaxHp) currentHp = MaxHp;
            if(currentHp < 0 ) currentHp = 0;
        }
    }

    // 최대 배터리
    public float MaxBt
    {
        get { return playerStatus.BT; }
    }

    // 현재 배터리
    public float CurrentBt
    {
        get { return currentBt; }
        set
        {
            currentBt = value;
            if(currentBt >= MaxBt) currentBt = MaxBt;
            if(currentBt < 0) currentBt = 0;
        }
    }
    #endregion

    private void Awake()
    {
        if (instance == null)
        {
            instance = this;
            DontDestroyOnLoad(this.gameObject);
        }
        else Destroy(this.gameObject);
    
        playerStatus = ScriptableObject.Instantiate(Resources.Load<PlayerStatus>("Prefabs/PlayerData/PlayerStatus"));

        // 치트키 변수 => 현재 공격력 저장
        befAtk = playerStatus.AtkIncrease;
    }

    private void Start()
    {
        Initialize();
    }

    // 초기화 함수
    void Initialize()
    {
        // 현재 체력과 배터리를 최대 체력과 배터리로 설정
        currentHp = MaxHp;
        currentBt = MaxBt;

        // 치트키 관련 변수
        isCheatHp = false;
        isCheatBt = false;
        isCheatAtk = false;
    }

    #region 치트키 함수
    // 치트키 중 무적을 클릭했을 때
    public void CheatHp()
    {
        isCheatHp = !isCheatHp;
    }

    // 치트키 중 배터리를 클릭했을 때
    public void CheatBt()
    {
        isCheatBt = !isCheatBt;
    }

    // 치트키 중 공격력을 클릭했을 때
    public void CheatAtk()
    {
        isCheatAtk = !isCheatAtk;

        // 치트 중이라면
        if (isCheatAtk)
        {   
            // 공격력 증가량을 100으로 설정하고, 공격력 포인트 1 증가
            playerStatus.AtkIncrease = 100;
            playerStatus.AtkPoint++;
        }
        // 치트를 비활성화하면
        else
        {
            // 공격력 증가량을 원상복귀 후, 공격력 포인트 원상복귀
            playerStatus.AtkIncrease = befAtk;
            playerStatus.AtkPoint--;
        }
    }
    #endregion

    private void FixedUpdate()
    {
        #region 치트
        // 체력이 치트 상태일 때,
        if (isCheatHp)
        {
            // 현재 체력을 최대 체력으로
            CurrentHp = MaxHp;
        }

        // 배터리가 치트 상태일 때, 
        if (isCheatBt)
        {
            // 현재 배터리를 최대 배터리로 
            currentBt = MaxBt;
        }
        #endregion

        // 플레이어가 해금되지 않은 장소를 갈 때, 체력 감소
        MapLockMinusHP();

        // 현재 체력이 0보다 클 때,
        if (currentHp > 0)
        {
            // 플레이어가 지금 집이 아니라면,
            if (!PlayerController.instance.isHouse)
            {
                // 배터리 감소 코루틴 호출
                if (currentBt > 0) StartCoroutine("MinusCurrentBt");
                else
                {
                    // 배터리가 0이되면 배터리 감소 코루틴 중지 및 체력 감소 코루틴 호출
                    StopCoroutine("MinusCurrentBt");
                    StartCoroutine("MinusCurrentHp");
                }
            }
            // 플레이어가 지금 집이라면,
            else
            {  
                // 현재 배터리가 최대 배터리보다 적다면, 배터리 증가 코루틴 호출
                if (currentBt < MaxBt) StartCoroutine("PlusCurrentBt");

                // 만약 현재 배터리가 최대 배터리만큼 찼다면, 배터리 증가 코루틴 중지
                else StopCoroutine("PlusCurrentBt");
            }
        }

        // 플레이어 죽음 처리
        else
        {
            if (!die)
            {
                // 죽었다고 표시
                die = true;

                // 체력 감소 코루틴이 실행 중이라면, 체력 감소 코루틴 중지
                StopCoroutine("MinusCurrentHp");
                
                // 인벤에 있던 모든 아이템을 삭제
                Inven.instance.RemoveAll();

                // 죽었을 때 UI를 활성화
                UiManager_.instance.Die();
            }
        }
    }

    // 배터리 증가 함수 => 플레이어가 기지 내부에 있을 때, 빠르게 배터리가 증가하도록 설정한다.
    IEnumerator PlusCurrentBt()
    {
        // plusBattery가 false일 때만 실행
        if (!plusBattery)
        {
            // plusBattery를 true로 설정하여 무한 반복을 방지
            plusBattery = true;

            // 1초 후
            yield return new WaitForSecondsRealtime(1.0f);
            
            // 현재 배터리 10 증가
            currentBt+= 10;

            // 만약, 현재 배터리가 최대 배터리보다 많다면, 현재 배터리를 최대 배터리로 설정
            if(currentBt > MaxBt) currentBt = MaxBt;
            
            // plusBattery를 false로 설정하여 다시 이 코루틴이 실행될 수 있도록 설정
            plusBattery = false;
        }
    }

    // 배터리 감소 함수 => 기본 1분에 1씩 감소하여 플레이어가 기지 외부에서 최대 10분동안 돌아다닐 수 있도록 설정한다.
    IEnumerator MinusCurrentBt()
    {
        // minusBattery가 false일 때만 실행
        if(!minusBattery)
        {
            // minusBattery를 true로 설정하여 무한 반복 방지
            minusBattery = true;

            // 60초 후
            yield return new WaitForSecondsRealtime(1f);

            // 만약 플레이어가 지금 하늘을 날고 있지 않으면, 배터리를 1만 감소시키고, 플레이어가 하늘을 날고 있다면 20을 감소시킴
            if (!PlayerController.instance.isFlying) currentBt--;
            else currentBt -= 20;

            // 현재 배터리가 0 미만이라면, 현재 배터리를 0으로 설정
            if(currentBt < 0) currentBt = 0;

            // minusBattery를 false로 설정하여, 다시 이 코루틴이 실행될 수 있도록 설정
            minusBattery = false;
        }
    }

    // 배터리가 없을 때 체력 감소
    IEnumerator MinusCurrentHp()
    {
        // minusHp가 false일 때만 실행
        if (!minusHp)
        {
            // minusHp를 true로 설정하여 무한 반복 방지
            minusHp = true;

            // 1초 후 
            yield return new WaitForSecondsRealtime(1.0f);

            // 현재 체력 5 감소
            currentHp -= 5;

            // 현재 체력이 0 미만이라면, 현재 체력을 0으로 설정
            if(currentHp < 0) currentHp = 0;

            // minusHp를 false로 설정하여 다시 이 코루틴에 실행될 수 있도록 설정
            minusHp = false;
        }
    }
    
    // 체력 감소 코루틴 => 해금되지 않은 맵을 갔을 때 호출
    IEnumerator MapLockMinusCurrentHP()
    {
        // minusHpMap이 false일 때만 실행
        if (!minusHpMap)
        {
            //minusHpMap을 true로 설정하여 무한 반복 방지
            minusHpMap = true;

            // 체력 10 감소 => 바로 바로 감소되어야 하기 때문에 WaitFor....전에 실행함
            currentHp -= 10;

            // 만약 현재 체력이 0 미만이라면, 현재 체력을 0으로 설정
            if (currentHp < 0) currentHp = 0;

            // 1초 후
            yield return new WaitForSecondsRealtime(1.0f);

            // minusHpMap을 false로 설정하여 다시 이 코루틴에 실행될 수 있도록 설정
            minusHpMap = false;
        }
    }

    // 체력 감소 코루틴 시작 및 중지
    void MapLockMinusHP()
    {
        // 플레이어가 현재 해금되지 않은 맵에 있다면
        if ((PlayerController.instance.isForest && !playerStatus.IsForestUnLock)
             || (PlayerController.instance.isDesert && !playerStatus.IsDesertUnLock)
             || (PlayerController.instance.isCave && !playerStatus.IsCaveUnLock))
            StartCoroutine(MapLockMinusCurrentHP());    // 체력 감소 시작
        else StopCoroutine(MapLockMinusCurrentHP());    // 아니라면, 체력 감소 중지
    }

    // 초기화 함수
    public void Reset()
    {
        currentHp = MaxHp;
        CurrentBt = MaxBt;        
    }
}
using UnityEngine;

public class StatusUi_ : MonoBehaviour
{
    // 스탯매니저를 가져올 변수
    StatusManager statusManager;

    // 현재 각 스탯값
    private UILabel hpLabel;
    private UILabel btLabel;
    private UILabel spLabel;
    private UILabel atkLabel;
    private UILabel gspLabel;

    // 현재 각 스탯들의 강화 상태 값(퍼센트)
    private UILabel hpPercent;
    private UILabel btPercent;
    private UILabel spPercent;
    private UILabel atkPercent;
    private UILabel gspPercent;

    // 현재 각 스탯들의 강화 상태 슬라이더
    private UISprite hpBar;
    private UISprite btBar;
    private UISprite spBar;
    private UISprite atkBar;
    private UISprite gspBar;

    // 현재 총 강화 상태를 숫자로 표시
    private UISprite circle;
    private UILabel upgradeLevel;

    float Hp;
    float Bt;
    float Sp;
    float Atk;
    float Gsp;

    int HpPoint;
    int BtPoint;
    int SpPoint;
    int AtkPoint;
    int GspPoint;

    private void Awake()
    {
        Initialize();
    }

    void Start()
    {
        VariableSetting();
        TextUpdate();
        PercentageUpdate();
        StatLevelGraph();
        CircleValueUpdate();
    }

    void Update()
    {
        VariableSetting();
        TextUpdate();
        PercentageUpdate();
        StatLevelGraph();
        CircleValueUpdate();
    }

    void Initialize()
    {
        statusManager = StatusManager.instance;

        hpLabel = GameObject.Find("NowHpLabel").GetComponent<UILabel>();
        btLabel = GameObject.Find("NowBtLabel").GetComponent<UILabel>();
        spLabel = GameObject.Find("NowSpLabel").GetComponent<UILabel>();
        atkLabel = GameObject.Find("NowAtkLabel").GetComponent<UILabel>();
        gspLabel = GameObject.Find("NowGspLabel").GetComponent<UILabel>();

        hpPercent = GameObject.Find("HpPercentage").GetComponent<UILabel>();
        btPercent = GameObject.Find("BtPercentage").GetComponent<UILabel>();
        spPercent = GameObject.Find("SpPercentage").GetComponent<UILabel>();
        atkPercent = GameObject.Find("AtkPercentage").GetComponent<UILabel>();
        gspPercent = GameObject.Find("GspPercentage").GetComponent<UILabel>();

        hpBar = GameObject.Find("HPFilled").GetComponent<UISprite>();
        btBar = GameObject.Find("BTFilled").GetComponent<UISprite>();
        spBar = GameObject.Find("SPFilled").GetComponent<UISprite>();
        atkBar = GameObject.Find("ATKFilled").GetComponent<UISprite>();
        gspBar = GameObject.Find("GSPFilled").GetComponent<UISprite>();

        circle = GameObject.Find("CircleValue").GetComponent<UISprite>();
        upgradeLevel = GameObject.Find("UpgradeLabel").GetComponent<UILabel>();

    }

    void VariableSetting()
    {
        Hp = statusManager.PlayerStatus.HP;
        Bt = statusManager.PlayerStatus.BT;
        Sp = statusManager.PlayerStatus.SP;
        Atk = statusManager.PlayerStatus.ATK;
        Gsp = statusManager.PlayerStatus.GSP;

        HpPoint = statusManager.PlayerStatus.HpPoint;
        BtPoint = statusManager.PlayerStatus.BtPoint;
        SpPoint = statusManager.PlayerStatus.SpPoint;
        AtkPoint = statusManager.PlayerStatus.AtkPoint;
        GspPoint = statusManager.PlayerStatus.GspPoint;
    }

    void TextUpdate()
    {
        hpLabel.text =  "HP : " + Hp.ToString();
        btLabel.text =  "BT : " + Bt.ToString();
        spLabel.text =  "SP : " + Sp.ToString();
        atkLabel.text = "ATK : " + Atk.ToString();
        gspLabel.text = "GSP : " + Gsp.ToString();
    }

    void PercentageUpdate()
    {
        hpPercent.text = (HpPoint * 20).ToString() + "%";
        btPercent.text = (BtPoint * 20).ToString() + "%";
        spPercent.text = (SpPoint * 20).ToString() + "%";
        atkPercent.text = (AtkPoint * 20).ToString() + "%";
        gspPercent.text = (GspPoint * 20).ToString() + "%";
    }

    void StatLevelGraph()
    {
        hpBar.fillAmount = (float)HpPoint / 5.0f;
        btBar.fillAmount = (float)BtPoint / 5.0f;
        spBar.fillAmount = (float)SpPoint / 5.0f;
        atkBar.fillAmount = (float)AtkPoint / 5.0f;
        gspBar.fillAmount = (float)GspPoint / 5.0f;
    }

    void CircleValueUpdate()
    {
        circle.fillAmount = (float)(HpPoint + BtPoint + SpPoint + AtkPoint + GspPoint)/ 25.0f;
        upgradeLevel.text = (circle.fillAmount * 100).ToString();
    }
}