배틀블루

양규빈·2023년 4월 21일

유니티

목록 보기
2/34

개요

시연 영상 1 링크
시연 영상 2 링크

시연 연상 최종본

깃허브

이전의 디펜스 게임이 PC 플랫폼 기반의 게임이었다면, 이번 게임은 모바일 환경의 게임을 구현해보려 합니다.
이름은 거창하지만, 블루아카이브나 프리코네 같은 수집형RPG게임을 모방하여 만들어보고자 합니다.

설계

  1. 각 씬 전환 간 매니저를 통하여, 호출하도록 한다.
  2. 로비씬은 게임 내부의 다른 씬들과 연결하는 허브 역할을 한다.
  3. 인게임씬은 플레이어의 실질적인 게임 플레이를 담당한다.
  4. 인게임은 캐릭터의 AI에 따라 자동으로 전투가 진행된다.
  5. 싱글턴패턴과 옵저버패턴, 데코레이터 패턴과 컴포넌트 패턴 등, 다양한 디자인 패턴을 사용하는 데 중점을 둔다.
  6. 상점과 퀘스트, 캐릭터를 관리하는 씬을 만듭니다.
  7. 캐릭터 데이터는 PlayerPrefs을 이용하여 씬 전환이나 게임이 종료되어도 사용할 수 있도록 합니다.

클래스 다이어그램 기법은 아니지만, 비슷한 구조도를 그려보았습니다.
해당 도표에서는 표현되지 못했지만, 뽑기와 캐릭터, 분대, 탐험, 퀘스트, 상점은 로비에서 버튼을 눌러서 이동합니다.

각 씬에 사용될 클래스를 고려하여 작성해보았습니다.

씬로더

  • 씬매니저 클래스 : 씬의 전환과 로딩, 로딩 프로세스의 UI 구현을 담당하는 클래스.
  • DontDestroy 클래스 : 오브젝트의 비파괴를 담당하는 클래스.

로비씬

  • UI버튼 클래스 : 로비에서 작동되는 UI 버튼들을 관리하는 클래스, 씬로더를 호출하여 씬의 전환을 담당한다.
  • 로비 매니저 클래스: 로비에서 사용하는 자원들을 관리하는 클래스, 피로도와 골드 등의 값을 저장.

뽑기 씬

  • 뽑기 매니저 클래스 : 보유한 자원을 이용하여 랜덤으로 유닛을 선택하여 플레이어의 캐릭터 리스트에 저장하는 클래스.
  • UI 버튼 클래스 : 뽑기 씬에서 작동하는 UI 버튼들을 관리하는 클래스.

캐릭터 씬

  • 캐릭터 클래스 : 캐릭터의 데이터 형을 초기화하는 클래스.
  • 캐릭터 매니저 클래스 : 사용자가 보유하고 있는 캐릭터들과 관련된 처리를 하는 클래스. 육성과 관련된 작업을 할 수 있다.
  • UI 버튼 클래스 : 캐릭터 씬에서 작동하는 UI 버튼들을 관리하는 클래스.

분대 씬

  • 분대 매니저 클래스 : 설정한 캐릭터들의 분대에 지정하여 저장하는 클래스.
  • UI 버튼 클래스 : 캐릭터 씬에서 작동하는 UI 버튼들을 관리하는 클래스.

탐험 씬

  • 탐험 매니저 클래스 : 보유한 캐릭터를 이용하여 탐험에 보내, 재화를 얻을 수 있도록 하는 클래스.
  • UI 버튼 클래스 : 탐험에서 사용하는 UI 버튼들을 관리하는 클래스.

퀘스트 씬

  • 퀘스트 매니저 클래스 : 저장된 퀘스트들 중에서 랜덤으로 n 개의 퀘스트를 플레이어게에 정정해주는 클래스.
  • UI 버튼 클래스 : 퀘스트 씬에서 사용하는 UI 버튼들을 관리하는 클래스.

상점 씬

  • 상점 매니저 클래스 : 캐릭터 육성에 필요한 재화나 장비들을 구매할 수 있도록 하는 클래스.
  • UI 버튼 클래스 : 상점 씬에서 사용하는 UI 버튼들을 관리하는 클래스.

인게임 씬

  • Enemy 클래스 : 적 오브젝트의 데이터를 관리하는 클래스.
  • Enemy AI 클래스 : 적 오브젝트의 이동과 공격에 관련된 클래스.
  • 전투 프로세스 클래스 : Enemy와 Character 간의 전투 흐름을 관리하는 클래스.
  • 캐릭터 AI 클래스 : 사용자가 보유한 캐릭터를 꺼내와 AI 작용을 입히는 클래스.
  • 인게임 매니저 클래스 : 인게임 씬에서 필요한 UI와 스테이지 관리 등을 담당하는 클래스.

스테이지 씬

  • 스테이지 매니저 클래스 : 스테이지 배열을 관리하는 클래스.

구현

로딩씬

개요

각 씬 전환 간 로딩을 담당하는 씬입니다.
이전에 만들었던 디펜스 게임을 경험으로 수월하게 만들 수 있었습니다.

열거형 변수를 이용하여, None과 Ing 상태로 나누어,
UI의 Set True/False를 조정했고,
Async을 이용하여, 비동기 작업의 진행 상태를 가져와 이미지와 텍스트를 이용해 로딩바와 퍼센트를 구현했습니다.

스크립트

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class SceneLoadManager : MonoBehaviour
{
    static SceneLoadManager instance;
    enum eLoadingState
    {
        NONE =0,
        ING,
    }

    [SerializeField]
    private Canvas ManagerCanvas;
    [SerializeField]
    private Image loadingBar;
    [SerializeField]
    private Text loadingText;

    string nextScene;
    private eLoadingState eCurrnetState;

    public static SceneLoadManager _instance
    {
        get { return instance; }
    }

    private void Awake()
    {
        instance = this;
        eCurrnetState = eLoadingState.NONE;
        DontDestroyOnLoad(this.gameObject);
    }
    
    private void Start()
    {
        SceneLoadder("01_LobbyScene_UI");
    }

    private void Update()
    {
        switch(eCurrnetState)
        {
            case eLoadingState.NONE:
                ManagerCanvas.gameObject.SetActive(false);
                loadingBar.gameObject.SetActive(false);
                loadingText.gameObject.SetActive(false);
                break;
            case eLoadingState.ING:
                ManagerCanvas.gameObject.SetActive(true);
                loadingBar.gameObject.SetActive(true);
                loadingText.gameObject.SetActive(true);
                break;
        }
    }

    public void SceneLoadder(string sceneName)
    {
        nextScene = sceneName;
        eCurrnetState = eLoadingState.ING;
        StartCoroutine(LoadSceneManager());
    }

    IEnumerator LoadSceneManager()
    {
        AsyncOperation async = SceneManager.LoadSceneAsync(nextScene);
        async.allowSceneActivation = false;
        float timer = 0f;
        while(!async.isDone)
        {
            yield return null;

            if(async.progress < 0.9f)
            {
                loadingBar.fillAmount = async.progress;

                int percent = Mathf.RoundToInt(async.progress * 100);
                loadingText.text = $"{percent}%";
            }
            else
            {
                timer += Time.unscaledDeltaTime;

                float percent = Mathf.RoundToInt(Mathf.Lerp(0.9f, 1.0f, timer) * 100);
                loadingText.text = $"{percent}%";

                loadingBar.fillAmount = Mathf.Lerp(0.9f, 1.0f, timer);
                if(loadingBar.fillAmount>=1f)
                {
                    async.allowSceneActivation = true;
                }
            }
        }
        eCurrnetState = eLoadingState.NONE;
    }
}

전체적인 구조는 개요에서 설명한 것과 같습니다.
SceneLoadder를 통해서 스트링 형태로 다음 씬의 이름을 입력받는다.
이후 코루틴 함수를 실행하여,
입력받은 이름의 씬을 불러온다.

async.progress는 현재 씬 로딩 진행도를 나타내며, 0.9보다 작을 때는 진행도를 바로 표시하고, 0.9 이상일 때는 시간에 따라 진행도를 점차 증가시킨다.

eCurrnetState가 NONE이면 로딩바, 로딩 텍스트 등의 UI가 숨겨지고, ING이면 보여진다.

로비씬

개요

설계에서 썼다시피, 로비씬은 보통 메인화면역할을 하고 있는 코드입니다.
게임과 상점, 분대, 캐릭터, 뽑기 등. 여러 씬으로 통할 수 있는 통로 역할을 함과 동시에, Gold와 Stamina, 그리고 myCharacter 등. 여러 데이터를 보유하고 있기에 중요한 씬이기도 합니다.

골드와 스테미너는 static을 이용해 전역변수화 하여, 다른 씬에서 필요할 때마다 호출하여 사용할 수 있도록 하였습니다.

사용자가 보유하고 있는 캐릭터들을 보관하는 리스트인 myCharacterList 역시도 전역변수화 하였습니다.

이러한 myCharacterList는 JSON파일에 데이터를 저장하여, 관리됩니다.
게임 시작 할 때, JSON파일이 저장하고 있는 데이터를 불러와 리스트에 저장하고,
게임 종료할 때, JSON파일에 리스트의 내용을 저장하여 수정사항을 반영합니다.

로비의 화면은 다음과 같이 구성되어 있습니다.
보면 알 수 있다시피, 프리코네와 블루아카이브의 구성창과 비슷합니다... :)

스크립트

Start()


Start 부분입니다.
'JsonFile/MyCharacter.json'에 프로그램의 주소+/Resource/를 덧붙여서 최종적으로 json파일을 가져옵니다.
해당 파일이 있을 경우에는 ReadAllText()를 이용하여 전체 스트링을 읽어들여와,
JsonConvert.DeserializeObject()함수를 통해서, JSON형식의 문자열을 객체로 역직렬화합니다.

역직렬화한 데이터를 chracterList에 임시 저장하고, 이후 myCharacterList에 넣어서 최종적으로 마칩니다.

반대로 해당 파일이 없다면, 디버그 메시지를 출력하고 넘깁니다.

OnApplicationQuit()

위 코드는, 게임을 종료할 때 데이터를 저장하는 로직입니다.
Start와 마찬가지로, 주소를 찾아가서 myCharacter파일을 찾아옵니다.
이후, 파일이 존재하지 않다면, File.WriteAllText()함수가 새로운 json파일을 만들고, 있다면 AppendAllText()를 통해, 데이터를 추가합니다.

수정사항


기존의 코드에서는 데이터가 중복되게 Json에 저장되는 문제가 있었습니다.
씬을 옮길 때마다, 로비 씬이 파괴되기 때문에 Start가 반복되어, Json에서 데이터를 계속해서 추가로 불러오는 문제가 있었으며,
게임 종료시, 데이터를 저장할 때 WirteAllText가 아닌 AppendAllText를 사용하는 경우, 기존의 데이터에 추가 데이터를 부착하기 때문에 데이터+데이터로 중첩되는 문제가 발생했습니다.
때문에, 데이터의 유무와 관계 없이, Wirte를 이용하여 덧씌우는 방식으로 바꾸었습니다.

모집 씬

개요

재화를 소모하여 랜덤한 캐릭터를 뽑는 곳입니다.
GOLD를 소모하여 랜덤으로 뽑은 캐릭터를 myCharacterList에 추가합니다.

전체 캐릭터들의 데이터는 JsonGeneraotr를 이용해 만든, Json파일로 저장되어 있으며, 필요한 데이터를 불러옵니다.

인게임 화면은 위와 같습니다.

스크립트

CharacterClass

      public enum eChracter_State
  {
      None =0,
      Move,
      Attack,
      Death,
      Max
  }

  public int characterHP;
  public int characterPower;
  public int characterDef;
  public string characterName;
  public int characterLevel;
  public eChracter_State chracterState;

  public ChracterClass(int hp, int pow, int def, string name, int level)
  { 
      characterHP = hp;
      characterPower = pow;
      characterDef = def;
      characterName = name;
      this.chracterState =eChracter_State.None;
      characterLevel= level;
  }
  public ChracterClass(int hp, int pow, int def, string name, eChracter_State state, int level)
  {
      characterHP = hp;
      characterPower = pow;
      characterDef = def;
      characterName = name;
      chracterState = state;
      characterLevel=level;
  }
  public ChracterClass()
  {
      this.chracterState = eChracter_State.None;
  }

캐릭터가 필요로 하는 데이터를 저장하는 함수입니다. 각 상황마다 필요로 하는 생성자를 만들어 두었습니다.
단순한 코드였지만 해당 클래스를 만들때, 오류때문에 제법 애를 먹었습니다.

후술할 Json제너레이터에서 json파일로 직렬화 할 때, 자꾸 에러가 났었습니다.

한참을 씨름한 결과, Class가 상속받은 MonoBehaviour 때문에 문제가 발생했다는 걸 알게 되었습니다.

MonoBehaviour를 제거하니, 거짓말처럼 에러가 사라졌었습니다...
이유는 MonoBehaviour 객체는 Unity 게임 엔진의 런타임 환경에서 생성되고 관리되기 때문입니다. JsonUtility 클래스는 C#의 기본적인 직렬화 방식을 사용하여 객체를 JSON 형식으로 직렬화하지만, MonoBehaviour 객체는 Unity의 런타임 환경에서만 유효하기 때문에 일반적인 방식으로 직렬화할 수 없었습니다.

JsonGenerator

JsonFile을 생성하는 클래스입니다.
전체 캐릭터 데이터를 포함하는 제이슨 파일을 생성할 때 쓴 스크립트입니다.
위 이미지에는 루타 객체 한 명 뿐이지만, 완성한 json파일은 10명분의 데이터를 보유하고 있습니다.

RouletteManager

뽑기와 관련된 전반적인 프로세스를 관리하는 클래스입니다.
위 코드는 파일입출력과 관련된 부분입니다.

Start에서는 전체 캐릭터 Json에서 데이터를 가져와 배열에 저장합니다.
JSON 문자열 json을 ChracterClass 배열 객체로 역직렬화합니다.

버튼을 누르면, 랜덤한 숫자를 굴려서 캐릭터를 선정합니다.
요구하는 골드 이상을 보유하고 있는 경우, 리스트에 뽑은 캐릭터를 추가합니다.

이후, 뽑은 캐릭터를 TextMeshProUGUI를 이용해 출력하고,
골드량을 수정합니다.

캐릭터 씬

개요

보유한 캐릭터의 목록을 나타내는 씬입니다.

인게임에서는 위와 같은 화면 구성으로 되어있습니다.
캐릭터의 이름과 레벨을 보여주며, 세부 내용은 상세버튼을 눌러서 출력시킬 수 있습니다.

오브젝트 작업

UGUI의 Scroll View를 이용하여, 구현했습니다.

위아래 스크롤만 구현하기 때문에, Vertical Scrollbar는 None으로 바꾸었습니다.
컴포넌트들은 Viewport의 자식 컴포넌트인 Content에 붙입니다.

스크립트

CharacterUI_Manager

Dictionary<GameObject, ChracterClass> buttonSetArray;

  [SerializeField]
  GameObject ButtonSet;
  
  [SerializeField]
  Transform contentTransform;

  [SerializeField]
  Transform scrollView;

  int count =0;

  void Start()
  {
      buttonSetArray = new Dictionary<GameObject, ChracterClass>();

      for(int i=0; i<UI_Manager.myCharacterList.Count; i++)
      {
          AddButtonToButton();

      }
              
  }

  public void ToLobbyClick()
  {
      SceneLoadManager._instance.SceneLoadder("01_LobbyScene_UI");
  }

  public void AddButtonToButton()
  {

      GameObject newButton = Instantiate(ButtonSet);
      buttonSetArray.Add(newButton, UI_Manager.myCharacterList[count]);
      Button buttonComponent = newButton.transform.GetChild(1).GetComponent<Button>();
      buttonComponent.onClick.AddListener(ButtonClickEvent);
      SetUI_Information(newButton);
      count++;

      // Content RectTransform을 가져옵니다.
      RectTransform contentRectTransform = scrollView.transform.GetChild(0).GetComponent<RectTransform>();

      // 추가할 UI 오브젝트의 RectTransform을 가져옵니다.
      RectTransform newButtonRectTransform = newButton.GetComponent<RectTransform>();

      // 추가할 UI 오브젝트의 크기를 스크롤뷰의 Content와 같게 설정합니다.
      newButtonRectTransform.sizeDelta = new Vector2(contentRectTransform.rect.width, newButtonRectTransform.rect.height);

      // 추가할 UI 오브젝트의 부모를 스크롤뷰의 Content로 설정합니다.
      newButtonRectTransform.SetParent(contentRectTransform);

      // 추가한 UI 오브젝트가 스크롤뷰의 하단에 위치하도록 Y 좌표를 조정합니다.
      float newButtonHeight = newButtonRectTransform.rect.height;
      float contentHeight = contentRectTransform.rect.height;
      newButtonRectTransform.anchoredPosition = new Vector2(newButtonRectTransform.anchoredPosition.x, -contentHeight - newButtonHeight);
  }

  public void AddButtonToTop()
  {
      // 버튼 인스턴스 생성
      GameObject newButton = Instantiate(ButtonSet, contentTransform);
      buttonSetArray.Add(newButton, UI_Manager.myCharacterList[count]);
      count++;
      // 버튼 위치 및 크기 조정
      newButton.transform.localPosition = Vector3.zero;
      newButton.transform.localScale = Vector3.one;
  }

  void SetUI_Information(GameObject newButton)
  {
      ChracterClass value;
      buttonSetArray.TryGetValue(newButton, out value);
      newButton.transform.GetChild(0).GetChild(0).GetComponent<TextMeshProUGUI>().text = value.characterName;
      newButton.transform.GetChild(0).GetChild(3).GetComponent<TextMeshProUGUI>().text = "레벨 : "+value.characterLevel.ToString();

  }

  public void ButtonClickEvent()
  {
      SceneLoadManager._instance.SceneLoadder_Additive("08_Character_Info");
  }

UI 작업을 관리하는 매니저입니다.
버튼과 백그라운드, 텍스트 등을 포함하는 게임오브젝트인 ButtonSet와 캐릭터 클래스를 보유하는 Dictionary.
스크롤 바의 위치를 참조하여 오브젝트의 위치를 조정하기 위한 직렬화 Tranform 변수 등을 선언합니다.

이후, Start에서 Character 리스트 내의 숫자 만큼 AddButtonToButton 함수를 호출하여 스크롤 뷰 상에 컴포넌트를 붙이도록 합니다.

AddButtonToButton함수는
Dictionary에 캐릭터 클래스와 세트 오브젝트의 값을 넣어주고 UI작업을 완료합니다.
이후, onClick.AddListener() 함수를 이용하여 ButtonClickEvent()함수를 연결시켜, 세부 창과 연동할 수 있도록 만들었습니다.

그 다음은 Instanticate로 생성된 오브젝트의 위치를 바꿔줍니다.
content의 Rect Transform와 세트 오브젝트의 Rect Transform를 가져와 오브젝트의 사이즈를 조절하고 하단부에 위치시킵니다.

newButtonRectTransform.anchoredPosition은 UI 오브젝트의 위치를 설정하는데 사용되는 변수입니다. 이 변수에 새로운 Vector2 값을 할당하여 UI 오브젝트의 위치를 변경합니다.

-contentHeight - newButtonHeight는 UI 오브젝트가 스크롤뷰의 하단에 위치하도록 하기 위해 계산된 값입니다.

newButtonRectTransform.anchoredPosition.x는 X축 위치를 그대로 가져오고,
contentHeight와 newButtonHeight를 더해 음수로 바꾸어 Y축 위치를 결정합니다.

이때 newButtonHeight는 새로운 버튼의 높이를 나타내고, contentHeight는 ScrollView의 Content의 높이를 나타냅니다.

추가사항_0430

캐릭터의 상세정보를 누르면 Character_Info창을 불러와, 데이터를 확인하고 장비를 착용할 수 있도록 하였습니다.
Character_Info는 SceneLoadder에서 Additive방식으로 씬을 불러와, 캐릭터 씬 위에 불러오도록 하였습니다.

캐릭터 인포 씬

개요

캐릭터 씬과 연결되는 씬입니다.
해당 씬에서는 캐릭터의 상세 정보를 보여주며, 장비 장착 및 업그레이드도 가능합니다.


인게임 화면에서는 위와 같이 구현됩니다.
(모든 아이템을 맥스레벨까지 착용한 상태입니다.)

레벨과 경험치, 그리고 성급을 보여주며
체력과 공격력, 방어력과 쿨타임도 보여줍니다.

오른편의 박스는 아이템 장착과 업그레이드를 담당하는 부분입니다.

오브젝트 작업

인포씬은 크게 두 개의 매니저와 두 개의 캔버스로 구성되어 있습니다.
Star는 캐릭터의 성급을 나타내는 이미지 박스입니다.
캐릭터 클래스 내의 데이터를 기반으로 SetTrue/False를 이용해 표현합니다.

오른쪽의 장비창은 기본적으로 SetOff상태이며, 장비 버튼을 클릭하면 On됩니다.


각 매니저들은 UI를 관리하다 보니 많은 컴포넌트들을 직렬화로 사용해야 했습니다.
이때, 되도록 배열을 이용하여 반복문으로 해결할 수 있도록 하였습니다.

스크립트

Character_InfoManager

캐릭터 인포 매니저입니다.
정적 변수로 받은 SELECT_CharacterData와 동일한 캐릭터를 myCharacterList내에서 찾아내어 메모리에 저장합니다.

그리고 해당 캐릭터의 모든 데이터를 참조하여 UI상에 띄웁니다.

이때, 큰 실수를 저질렀었는데요.

바로 myCharacter 내의 데이터가 아닌 정적 변수의 데이터를 불러왔습니다.
당시에는 큰 문제가 없어서, 별 생각없어 넘어갔었는데,
추후 아이템 착용 매니저에서 아이템을 장착하여 캐릭터 데이터에 직접적으로 데이터를 추가하는 과정에서 의도한대로 데이터가 보여지지 않아서, 지금까지 짠 코드가 잘못되었다는 것을 깨닳았습니다.
덕분에 많은 걸 배웠던 것 같습니다.

EquipmentManager

장비 착용과 업그레이드와 관련된 기능을 담고 있는 클래스입니다.


캐릭터 UI창의 장비 버튼을 눌렀을 경우, 유저가 클릭하여 선택한 장비와 관련된 데이터를 출력합니다.
가장 먼저, myCharacterList 내의 데이터를 확인하여, 선택한 장비에 해당하는 아이템을 가지고 있는지 확인합니다.
selectCharacItem이 바로 선택한 장비의 Null 유무를 확인하기 위한 클래스 변수입니다.

즉, 프로세스의 흐름은 아래와 같습니다.
캐릭터 UI 창에서 상세보기를 원하는 캐릭터를 클릭한다. -> 캐릭터 Info창이 열린다. -> 인포창에서 플레이어가 강화할 장비에 해당하는 버튼을 누른다. -> Equpment 창이 나타난다. -> 선택한 캐릭터가, 선택한 장비를 착용하고 있는지 확인한다. -> 장비를 착용하지 않았을 경우, myItemList 안에 해당 아이템을 가지고 있는지 검사한다. -> 장비가 있다면 NextItemImg와 Text를 수정한다. -> 다음 레벨 장비를 검사 하고 UI로 나타낸다. -> 맥스단계까지 반복.

이어서 위 코드는 현재 등급의 장비에 따라, 다음 레벨의 장비를 탐색하고 표현하는 로직입니다.
장비의 유무에 따라 아이템의 색상과 툴팁을 다르게 나타냅니다.

다음은 업그레이드 버튼 함수입니다.

UI로 다음 레벨과 현재 레벨의 아이템을 표현하였으면, 실제적으로 버튼을 눌러 아이템 등급을 강화하는 로직이 필요할 것입니다.

업그레이드 함수는, 먼저 플레이어의 아이템이 널인지 아닌지부터 판단합니다.
널인 상태에서 ItemGrade를 불러올 경우 예외처리가 되기 때문입니다.
장비가 null이라면 0등급의 장비를 탐색하여 ChracterClass의 Characteritems배열에 장비 인덱스를 인자로 받아 itemClass를 값으로 넣습니다.

null이 아닐 경우, 현재 캐릭터의 아이템 등급에 +1을 더한 int형 변수를 이용하여 아이템 리스트 내에 다음 등급의 장비가 있는지 검사하고 존재할 경우에는 아이템의 중복 개수 여부(Count)를 확인하여, 1일 경우 원소 제거. 1 이상일 경우 Count--를 하여 장비를 제거합니다.

실제적으로 유저의 스텟을 강화하는 부분은 이쪽입니다.
매개변수로 입력받는 값은 업그레이드한 아이템의 등급입니다. selectIndexPoint는 최초 버튼을 클릭하였을 때, 람다식으로 받은 각 버튼별 설정 인트값입니다.
순서대로 0- 쉴드, 1-아머, 2-도끼, 3-쿨타임으로 지정되어있으며.
설정 인트값을 동적변수로 참조받아, 열거형 형식 타입으로 변환하여 스위치문으로 분기를 돌립니다.

각 분기에 알맞은 스텟 수정 함수에 아이템 등급을 인자로 넘겨줘 최종적으로 캐릭터의 스텟을 조정합니다.

상점 씬

개요

캐릭터가 착용할 수 있는 장비를 구매할 수 있는 상점 씬입니다.
1~5 단계의 장비가 존재하며, 스크롤을 이용하여 단계를 조정하여 구매 버튼으로 장비를 구매할 수 있습니다.

스크롤을 이용한 상호작용과 UI 작용 구현 등에 굉장히 골머리를 앓았습니다...

오브젝트 작업

오브젝트 작업에서 가장 주의해야하는 부분은 하이어라커 상 오브젝트 배치에 따른 UGUI의 우선순위 조정이었습니다.

사용자가 직접 상호작용을 해야하는 부분인 스크롤과 버튼 부분에 집중했습니다.

Asset의 특성 상, X축과 Y축을 조절하였는데, 어느 한 축을 채우고 난 뒤 투명한 UI가 다른 UI를 덮어버리는 불상사가 발생하기도 했습니다.

또한, 구매 버튼의 카트 이미지를 조정하다 보니, 버튼 이미지가 제 의도에 맞지 않게 커져서 스크롤 부분을 침범하는 문제도 발생했었습니다.

그렇기 때문에 카트 이미지를 버튼과 분리하고, ItemBox의 순서를 조정하여 버튼과 스크롤을 침범하지 않도록 만들었습니다.

스크립트

상점 씬을 관리하는 ShopManager의 변수 선언 및 초기화 부분입니다.

각 박스에서 사용될 button들과 GoldText, 그리고 Scroll들을 SerializeField를 이용한 직렬화로 초기화 합니다.

itemPrice와 myItemPrice, selectedItemIndex는 각각 아이템의 가격 테이블, 그리고 각 아이템 박스가 현재 띄워야 할 아이템 가격, 아이템 가격을 선택하는 데 사용할 index 입니다.

Start에서는
버튼 배열인 buttons에 AddListenr를 이용해 스크립트 컴포넌트를 부착합니다.
이후, scrollViews의 원소에 스크롤 이벤트 컴포넌트도 추가합니다.

처음에는 index를 이용하여 하나의 함수를 이용해, 모든 Scroll 이벤트 함수를 제어 하려 했으나, Vector2 scrollPosition값을 매개변수로 입력해야하는 문제 때문에, 개별 함수를 만들어 추가했습니다.

다음은 플레이어가 스크롤을 드래그한 위치 값을 계산하여 selectedItemIndex에 저장하는 함수입니다.

slectedImageIndex은 Clamp함수를 이용하여, -1 ~ images의 크기 사이의 값이 나오도록 합니다.
(최초에는 0이 아닌 1을 start값으로 주었으나, 이 경우 selectedItemIndex에 0번 원소를 넣지 않는 문제가 있었기에, 0으로 수정했습니다.)

Imagess 배열은 scrollView의 [0]번째 배열이 가지고 있는 Image 타입의 모든 자식 컴포넌트들을 저장합니다.
이후, 해당 자식 객체들의 좌표 값을 positions에 저장합니다.

이렇게 저장한 값은 사용자 정의 값을 비교하는 데 쓰는 기준값이 됩니다.

scrollPosition(사용자 드래그 값)에 scrollView[0]의 위치 값을 더하여 사용자가 움직인 수치만큼의 벡터 값을 변수화 합니다.

이후, 해당 변수를 전체 자식 객체들의 좌표 값과 대조하여 가장 근사값(Distance)을 구합니다.

그리고 그 근사값을 지닌 자식 객체의 번호를 selectedImageIndex로 저장하여 최종적으로 인덱스를 구합니다.

해당 함수를 구현하는 데 상당히 오랜 시간이 걸렸었습니다.
버그도 많이 났었죠.

가장 기억에 남는 버그는 position[i]의 값을 초기화 하는 부분이었습니다.
처음에는 아래와 같이 스크롤 좌표에 자식 컴포넌트의 좌표를 더하는 방향으로 코드를 짰었습니다. images[i].transform.position + scrollViews.content.position.
하지만 스크롤을 내릴 때, 1의 값부터 1800이 찍히고, 3 이후부터는 MAX값이 찍히는 오류가 발생했었습니다.

이 부분을 해결하는 데 굉장히 시간을 썼던 거로 기억합니다.
정작, scrollView.content.position을 지우니까 해결되는 걸 보니 기쁘면서도 한편으로는 바보짓을 했다는 생각이 나더군요.

구매 부분입니다.
버튼에 연결되는 함수이죠.

SetItemPrice()함수는 OnScrollViewValueChanged()함수가 구한 Index 배열내의 함수를 차조하여 myPrice의 가격을 바꿔줍니다.
그리고 전체 GoldText를 변환시킵니다.

BuySelectedItem 부분은 아직 완벽히 구현하지 않은 상태입니다.
전역변수인 GOLD를 가져와 골드 값을 대조하고, 구매할 경우 json파일 형태로 구매한 아이템 데이터를 저장하는 형태로 구현할 예정입니다.

추가사항_0430

//0==방패, 1==갑옷, 2==도끼, 3==쿨감
  public void BuySelectedItem(int itemIndex)
  {
      SetItemPrice();
      //선택한 아이템 구매 처리
      int price = myItemPrice[itemIndex];

      if (UI_Manager.GOLD >=price)
      {
          //필요한 변수 초기화
          ItemClass buyItem = new ItemClass();
          int index = selectedItemIndex[itemIndex];
          ItemClass.eItemName name = (ItemClass.eItemName)itemIndex;

          //람다식을 이용하여 조건에 일치하는 객체를 검색합니다.
          ItemClass Find_Item = UI_Manager.myItemList.Find(item => item.itemName == name && item.itemGrade == index);
          if (Find_Item != null)
          {
              //조건에 맞을 경우,
              Find_Item.itemCount++;
          }
          else
          {
              //조건에 맞지 않을 경우,
              buyItem.itemName = name;
              buyItem.itemGrade = index;
              UI_Manager.myItemList.Add(buyItem);
          }
          Debug.Log("구매 성공!");

          UI_Manager.GOLD -= price;
      }
      else
      {
          Debug.Log("골드 부족");
      }

      haveGoldlabel_text.text = "골드 : " + UI_Manager.GOLD.ToString();

      foreach (ItemClass item in UI_Manager.myItemList)
      {
          Debug.Log(item.itemName +" -  grade:" + item.itemGrade + " count : "+item.itemCount);
      }

  }

아이템 구매 부분입니다.
먼저, SetPrice함수를 이용하여 배열 내의 가격을 재셋팅합니다.
itemIndex는 람다식을 이용한 버튼 컴포넌트 부착 때, 넘겨받은 인자입니다.

가장 먼저 플레이어가 보유한 골드가 요구하는 골드량보다 많은지 검사합니다.
참일 경우 아이템 구매 분기에 들어갑니다.

각, 아이템의 고유한 int를 버튼마다 지니고 있습니다.
먼저 구매할 아이템이 존재하는지 확인하기 위해서 필요한 변수들을 초기화 합니다.

아이템을 검색하는 데는 이름과 등급. 두가지 값이 필요합니다.

myItemList는 리스트로 저장되어 있기 때문에, Find 함수를 이용하여 일치하는 값이 있는지 확인할 수 있습니다.

만약, null값인 경우 새로운 아이템을 List에 Add해줍니다.
아이템이 있다면, Count를 증감연산하여 중복하는 아이템 갯수를 늘려줍니다.

탐험씬

개요

보통의 모바일 게임의 경우에는 보유한 캐릭터를 탐험을 보내어, 재화를 벌어오도록 합니다.
해당 기능을 담당하는 씬입니다.

설계

고려사항
탐사지역은 총 네 개이다. (0-아메리카 1-아프리카 2-유럽 3-아시아)
Json파일인 MyDatas의 위치는
string path = "JsonFile/MyDatas의.json";
string fullPath = Application.dataPath + "/Resources/" + path; 와 같다.

설계
1. 최초 씬 시작 시, Exploring 상태가 아닌 캐릭터들을 선택 버튼으로 Set하고 비활성화한다.
2. 월드맵의 탐사지역과 탐사현황창에 MyDatas.json파일 내의 데이터를 가져와 갱신한다.

  • 특정 탐사지역이 차있을 경우, 버튼을 originSprites로 바꾼다.
  • 특정 탐사지역이 차있을 경우, 탐사현황 창의 텍스트 오브젝트에, 해당 캐릭터의 이름을 적는다.
  1. 월드맵의 탐사 지역 버튼을 누르면 하단에 캐릭터 선택 버튼들이 나열된다.
  2. 버튼을 클릭하면, 해당 지역에 캐릭터를 탐험을 보낸다.
  • Dictionary 내의 캐릭터 데이터와 버튼은 제거가 된다. 이때 제거한 데이터와 버튼을 스크롤뷰에도 반영한다.
  • CharacterClass 내의 eCharacterState를 Exploring 상태로 바꾼다.
  • jsonFile인 MyDatas에 탐사를 보낸 캐릭터와 탐사지역을 세트로 묶어서 저장한다.
  • 하단의 버튼 스크롤뷰는 다시 비활성화 된다.
  1. 탐사를 보낸 지역은 버튼의 스프라이트를 originSprites로 바꾸고 현환창의 텍스트에 이름을 갱신한다.

오브젝트 작업

  1. ButtonSet : 스크롤뷰에서 Content로 사용할 프리팹.
  2. Dispatch Points : 각 월드에 탐사할 지역을 클릭할 수 있는 버튼들의 집합.
  3. Finished Adventures Btns : 탐사를 완료하였을 때, 재화를 인컴 받고 데이터를 초기화할 수 있도록 플레이어가 누를 수 있는 버튼들의 집합.
  4. Site Ing Texts : 탐사중일 때, 탐험에 나간 캐릭터의 이름을 나타내고, 탐험이 끝났을 경우 완료 메시지를 출력할 수 있는 Text 컴포넌트들의 집합.
  5. Origin Sprites/Target Sprites : 탐사를 보내지 않았을 때, 탐사를 마쳤을 때 각각 버튼의 스프라이트를 바꾸기 위해 입력받은 Sprite 파일.
  6. Scroll View : 탐험을 보낼 수 있는 캐릭터들을 ScrollView로 나타내기 위해 받은 컴포넌트.
  7. RemainedTimes : Dispatch Points에 남은 시간을 표기하기 위해 필요한 TextMeshProUGUI 컴포넌트들의 집합.

스크립트 작업

AdventureManager

AdventureManager는 탐험 씬의 UI 관리 및, 프로세스와 관련된 로직을 담은 스크립트입니다.

Awake에서는 필요한 변수들을 초기화하는 작업을 합니다.

dispatchPoints는 월드맵에 놓인 탐사지역 버튼들을 저장한 배열입니다.
SelectDispatchPosBtnClick(int index) 버튼에 함수를 연결하여 각 지점마다 번호를 할당합니다.

finishedAdventureBtns[]는 UI창 우측에 배치된 버튼들을 저장하는 배열입니다.
탐험을 완료했다면, ExploringBtnClcik() 함수가 일치하는 인덱스 번 째의 탐사 완료 프로세스를 진행합니다.

AddbuttonFunc()은 UI 세팅과 관련된 함수입니다.
myCharacterList 내의 캐릭터 클래스 데이터들을 Dictionary 타입의 자료형인Dictionary<GameObject, ChracterClass> buttonSetArray;에 저장하고 버튼을 생성하고 캐릭터 데이터를 매칭하는 로직을 담당합니다.

BehindBtnScroll은 단순히, UI버튼들을 SetActive(false)하는 작업을 하는 코드입니다.

플레이어가 탐사지역을 클릭했을 때, UI버튼들이 하단부에 나열되기 때문이죠.

DispatchPointRenew는 캐릭터 데이터 내에 현재 상태를 파악하여 탐사중인지 아닌지를 검사하고, 탐사중, 탐사완료에 따라 분기를 나누어 작업합니다.

  1. disPatchPointIndex
    위에서 설명했다시피, 변수인 disPatchPointIndex 변수에 버튼에 할당된 index값을 전달하여, 클래스 내 맴버 함수가 관련된 작업을 할 수 있도록 합니다.
    그리고 버튼 객체들을 On하여, 플레이어가 버튼을 눌러 탐험을 보낼 캐릭터를 선택할 수 있게 합니다.

  2. AddButtonFunc은
    UI작업을 하는 AddButtonFunc은 기본적인 로직의 구조는 CharacterUI_Manager에서 설명했던 AddButtonToButton()함수와 동일합니다.
    다만, 캐릭터UI의 경우에는 Vertical 타입의 ScrollView에서 작동되는 구조였고 반대로 어드벤쳐의 경우엔 Horizontal 타입의 ScrollView에서 작동된다는 점이 다르기에, 코드상에서 일부분의 변경점이 있었습니다.
    또 다른 점이 있다면, 탐험의 경우에는 하나의 포인트에 한 명의 캐릭터만 보낼 수 있기 때문에(1-1 관계), 탐험 중인 캐릭터는 State변수를 참고하여 목록에서 제외한다는 점에서 차이가 있겠습니다.

    스크롤 뷰 상에서 세부 차이점을 살펴보자면,

    CharacterUI_Manager::AddButtonToButton()의 경우에는 anchoredPosition.y 값에 (-contentHeight - newButtonHeight) 값을 할당하여 스크롤뷰의 아래쪽에 Content 프리팹의 위치를 옮기고,

    AddButtonFunc은 y값은 유지하면서 anchoredPosition.x 값에 (contentWidth + newButtonWidth) 값을 할당하여 오른쪽으로 Content 프리팹의 위치를 옮긴다는 점에서 차이가 있습니다.

  1. BehindBtnScroll
    앞서 설명했다시피, 반복문을 이용하여 버튼 컴포넌트들의 SetActive를 False로 바꾸는 단순한 기능을 가진 함수입니다.

  2. DispatchPointRenew
    설명에 앞서 characterAdventure란 캐릭터가 탐험을 떠난 포인트 지점(0~3)을 저장하는 변수입니다. (기본 값은 -1이다.)
    이번 Adventure씬을 설계하면서 캐릭터 클래스에 추가한 변수이기도 합니다.

    siteIngTexts은 우측의 버튼창에 표시되는 텍스트 배열입니다.
    완료유무, 혹은 탐험중인 캐릭터명을 표시합니다.

myCharacterList를 순회하며 캐릭터의 CharacterAdventure가 범위 내에 해당하는 경우, 탐험 캐릭터 배열에 저장합니다.
그리고 버튼의 이미지를 변환하여 탐험 중임을 나타내며, 텍스트를 변경합니다.

마지막으로는 현재 DateTime과 캐릭터의 탐험 종료 시간을 비교하여, 완료 여부를 배열에 저장합니다.
RemainTimeShow()함수는 InvokeRepeating을 이용하여 반복되는 함수이며, 월드맵에 남은 시간을 표시할 수 있도록해줍니다.

  1. ExploringBtnClick

    탐험 완료시에 버튼을 눌러, UI 작업과 데이터 작업을 하는 함수입니다.
    myCharacterList 내의 캐릭터 데이터 중에서 AdventureCharacters[index]의 캐릭터와 일치하는 데이터를 Find합니다.
    캐릭터의 모험과 관련된 변수들을 모두 초기화합니다.
    State를 None으로 바꾸고, Adventure변수는 -1로, 시간 역시도 MinValue로 바꿉니다.

    그리고 월드맵 탐사지역의 버튼을 원래 Sprite로 변경하여, 탐사가 끝나서 다시 보낼 수 있음을 가시적으로 보여줍니다.

    탐사 보상 부분은 아직 구현하지 않았지만, 골드나 스테미너, 혹은 다른 재화를 추가하여 보상을 줄 수 있을 것입니다.

    UI작업 후에는 데이터 Clear작업을 합니다.
    반복문을 돌려,ButtonSetArray 내의 모든 버튼들을 파괴합니다.
    그리고 Dictionary 내의 모든 데이터를 깨끗이 제거합니다.
    다음은 반복문을 돌려, 다시 AddButtonFunc()함수를 이용하여 버튼을 Set합니다.
    즉 Start()함수의 초기화 부분을 다시 하여, 플레이어가 게임을 이어서 할 수 있도록 만드는 것입니다.

  2. SelectCharacButtonClick

    캐릭터 버튼을 눌러서 탐사지역에 선택한 객체를 탐험을 보내는 기능을 가진 함수입니다.
    GameObject clickedButton = UnityEngine.EventSystems.EventSystem.current.currentSelectedGameObject를 이용하여 현재 플레이어가 선택한 버튼의 객체를 가져옵니다.
    그리고 그 버튼 객체를 key값으로 하여, Dictionary 타입의 변수에 해당하는 캐릭터 데이터를 가져오도록 합니다.

    다음은,
    가져온 데이터를 이용하여, myCharacterList에서 일치하는 캐릭터를 Find하여 탐사 상태로 수정하는 작업을 합니다.

    끝으로,
    반복문을 돌려 buttonSetArray내에 값이 수정한 캐릭터 데이터와 일치하는 key값을 가져와 Destroy하고 데이터도 제거합니다.

    이후, 버튼들을 Behind하여, Active를 false로 바꾸고 DispatchPointRenew()함수를 호출하여, characterAdventure() 리스트를 최신화합니다.

profile
훌륭한 개발자를 꿈꾸는 중입니다

0개의 댓글