XR플밍 - 12. UnityEngine3D Reactive 프로그래밍 - 기업협약 프로젝트 14일차 (9/4)

이형원·2025년 9월 4일
0

XR플밍

목록 보기
186/215

1. 금일 한 업무

오늘은 꽤 많이 작업했다.

  • 캐릭터 조각 획득/강화 시스템 임시 구현
  • 가챠를 통한 캐릭터 획득 및 조각 획득 구현
  • 캐릭터 정보 DB연결을 위한 리서치
  • 번외 - 트럭 애니메이션 만들기

2. 문제의 발생과 해결 과정

2.1 캐릭터의 데이터를 분류하기

가챠로 캐릭터를 중복으로 뽑았을 때 조각을 획득하고, 해당 조각을 활용하여 캐릭터의 레벨을 올리는 것이 가능하다.
이것이 이 게임에 있어서 기본적인 시스템이다.

다만 이와 같은 시스템을 개발하기 위해서는 어떻게 해야 할까? 우선 데이터의 구성부터 살펴봐야 한다.

캐릭터 데이터를 만드는 담당자가 따로 있었기 때문에 해당 데이터를 분석하면서 문득 생각했다.
스크립터블 오브젝트로 많은 변수를 구성해 놓았지만 아직 캐릭터의 획득 여부, 보유 조각 수 등의 요소가 없었기에, 해당 요소를 분석하면서 아래와 같이 데이터 구조를 설계할 수 있지 않을까 생각했다.

앞으로 유저별 변동이 없는 데이터를 기본 데이터, 유저별 변동이 있는 데이터를 유저 데이터로 분류할 것이다.

이와 같은 방식으로 구성하면, 당장 캐릭터 작업을 하는 분과 내가 작업하게 될 캐릭터 강화 부분의 내용을 나누어서 진행하기 때문에 협업하는 과정이 편하고, 변동이 비교적 적은 기본 데이터와 변동이 잦은 유저 데이터를 따로 관리하여 유지 보수성에 용이할 것으로 기대된다.

이에 따라서 해당 구조로 데이터를 짜 보는 것이 어떻겠냐고 담당자에게 제안했고 괜찮아 보인다는 답변을 받았다. 우선은 내가 강화 관련 데이터를 작성하고, 나중에 해당 두 데이터를 합치는 과정으로 설계를 하자는 방향을 정했다.

2.2 데이터의 구성 - 등급/레벨별 요구 조각 수 및 강화 데이터

먼저 기획팀에 캐릭터의 레벨별/등급별 요구 조각 개수에 대한 테이블을 달라고 했다. 그 결과 해당 기획 담당자가 이와 같이 자료를 보내주었다. 확정된 자료는 아니고 임시로 사용할 자료이다.

등급별, 레벨별 데이터가 따로 있기 때문에 우선은 임시로 이와 같이 데이터를 구성했다.

[CreateAssetMenu(fileName = "Unit_LevelUpData", menuName = "Data/Temp/Unit_LevelUpData")]
public class LevelUpData : ScriptableObject
{
    public List<RequirePiece> RequirePieceData = new List<RequirePiece>();
}

[Serializable]
public class RequirePiece
{
    public Grade Grade;
    public List<PieceLevelRatio> LevelRatio = new List<PieceLevelRatio>();
}

/// <summary>
/// 에디터상 입력을 위해 임시로 List 로 처리함. 추후에 Dictionary로 전환할 필요성 있음
/// </summary>
[Serializable]
public class PieceLevelRatio
{
    public int Level;
    public int RequirePiece;
}

현재 제공된 데이터가 임시 데이터이기도 하고, 데이터 테이블로 준 게 아닌 그냥 PPT로 만들어준 거라 일단은 이렇게 만들었다. 하지만 나중을 생각하면 이 구조를 Dictionary로 바꾸는 것이 검색 효율도 좋고 더 간단하게 표현할 수 있을 것이다. 다만 현재 상황에서는 에디터 상으로 데이터를 직접 입력해야 하는 상황이다 보니 리스트로 처리혔다.

해당 데이터는 아래와 같이 입력할 수 있다.

이와 같이 데이터를 입력하고, 다음으로 유저가 가지고 있어야 할 변동 데이터를 다음과 같이 저장했다.

using System;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName = "Unit_TempUpgradeUnitData", menuName = "Data/Temp/Unit_TempUpgradeUnitData")]
public class TempUpgradeUnitData : ScriptableObject
{
    // 해당 캐릭터 등급 -> 이후 UnitData에서 직접 참조하는 방식으로 변경
    public Grade Grade;
    // 캐릭터의 획득 여부
    public bool IsCollected;
    // 캐릭터의 업그레이드 레벨
    public int UpgradeLevel;
    // 현재 보유 캐릭터 조각 수
    public int CurrentPieces;

    // 캐릭터 강화 요구 조각 수 데이터 -> 이후 UnitData로 옮기는 방법 고민중
    [field:SerializeField] public LevelUpData LevelUpData { get; private set; }
    
    public int GetRequiredPiece()
    {
        if (UpgradeLevel >= 10) return 0;

        if (LevelUpData == null)
        {
            Debug.LogError($"[{name}] LevelUpData가 설정되지 않았습니다.");
            return 0;
        }

        RequirePiece requirePiece = LevelUpData.RequirePieceData.Find(r => r.Grade == Grade);
        if (requirePiece == null)
        {
            Debug.LogError($"[{name}] {Grade} 등급에 맞는 RequirePiece 데이터가 없습니다.");
            return 0;
        }

        PieceLevelRatio pieceLevelRatio = requirePiece.LevelRatio.Find(l => l.Level == UpgradeLevel + 1);
        if (pieceLevelRatio == null)
        {
            Debug.LogError($"[{name}] {Grade} / {UpgradeLevel}에 맞는 PieceLevelRatio 데이터가 없습니다.");
            return 0;
        }

        return pieceLevelRatio.RequirePiece;
    }

    public void AddPiece(int piece)
    {
        CurrentPieces += piece;
        Debug.Log("데이터 변동");
    }

    public void LevelUp()
    {
        // 최대레벨 변수 추가?
        if (UpgradeLevel >= 10) return;

        int requiredPiece = GetRequiredPiece();
        
        if(CurrentPieces >= requiredPiece)
        {
            CurrentPieces -= requiredPiece;
            UpgradeLevel += 1;
        }
    }
}

데이터는 스크립터블 오브젝트로 구성했으며, 스크립터블 오브젝트로 저장된 데이터의 변동이 에디터의 재생 종료과 관계없이 영구적으로 저장된다는 점을 오히려 역으로 이용했다.

플레이어의 변동 데이터 - 특히 조각을 통해 강화한 데이터의 경우, 레벨 등의 수치가 다시 내려가는 경우가 없을 것이기 때문에 영구 데이터로 저장하는 것이 오히려 유리하다고 판단했다.
또한 데이터가 영구적으로 변하는 것이기 때문에 이후 게임 씬 등의 적용에 있어서도 편리하게 작용할 수 있을 것이라 판단했다.

이제 테스트용으로 해당 데이터를 TempDataManager에 저장하고, 조각을 더하거나 레벨업하는 과정을 TempDataManage에서 처리하도록 했다.

using System.Collections.Generic;
using UnityEngine;

public class TempDataManager : MonoBehaviour
{
    #region Singleton

    public static TempDataManager Instance { get; private set; }

    private void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject);
            return;
        }

        Instance = this;
        DontDestroyOnLoad(gameObject);
        Init();
    }

    #endregion

    #region Data

    private int _selectedPresetIndex = 0;
    public int SelectedPresetIndex => _selectedPresetIndex;

    private List<TeamPresetData> _presetData = new List<TeamPresetData>();
    public List<TeamPresetData> PresetData => _presetData;

    [SerializeField] private TempUpgradeUnitData _upgradeUnitData;
    public TempUpgradeUnitData UpgradeData => _upgradeUnitData;

    #endregion    

    private void Init()
    {
        PresetDataInit();
    }

    #region Preset

    private void PresetDataInit()
    {
        if (_presetData.Count == 0)
        {
            for (int i = 0; i < 2; i++)
            {
                _presetData.Add(new TeamPresetData(5));
            }
        }
    }

    public void CreatePreset(int size)
    {
        _presetData.Add(new TeamPresetData(size));
    }

    public TeamPresetData ReadCurrentSelectedPreset()
    {
        if (_selectedPresetIndex == -1) return null;

        return _presetData[_selectedPresetIndex];
    }

    public void SelectPresetIndex(int index)
    {
        _selectedPresetIndex = index;
    }

    #endregion

    public void AddPiece()
    {        
        _upgradeUnitData.AddPiece(100);
    }

    public void LevelUp()
    {
        _upgradeUnitData.LevelUp();
    }
}

이와 같은 TempDataManager의 내용도 나중에 DB와 연동지어야 할 것이다.

2.3 캐릭터 정보를 DB와 연결하기 - 리서치

방법은 크게 두 가지, 혹은 한 가지 방법에서 분할하자면 총 세 가지 방법이 있어 보임

1. 캐릭터의 기본데이터는 DB에 올리지 않고(클라이언트에 저장), 유저별 캐릭터의 획득여부(level=0으로 대체) 및 조각 개수, 레벨만 유저별로 DB에 저장함.

  • 파이어베이스 데이터 작성 예시
    • 캐릭터의 ID를 기준으로 저장, 미획득은 레벨 0으로 표기
{
  "users": {
    "user123": {
      "characters": {
        "0": { "pieces": 12, "level": 3 },
        "1": { "pieces": 0, "level": 0 },
        "2": { "pieces": 100, "level": 10 }
        ...
      }
    }
  }
}
  • 장점

    • 캐릭터가 더 늘어날 예정이 없으며, 규모가 작은 게임에서는 충분히 고려할 만한 방식
    • 단순하고 직관적.
  • 단점

    • 추후 캐릭터가 추가될 경우 확장하기 어려움(확장성 부분에서 손해)
    • 획득하지 않은 캐릭터의 데이터도 모두 포함되어 데이터 낭비가 일어남.

2. 유저별 캐릭터 데이터 저장에 있어서, 획득한 캐릭터의 데이터만 저장

  • 파이어베이스 데이터 작성 예시(공통)
{
  "users": {
    "user123": {
      "characters": {
        "0": { "pieces": 12, "level": 3 },
        "2": { "pieces": 45, "level": 5 }
      }
    }
  }
}

여기서 기본 데이터를 DB로 올릴 거냐, 안 올릴 거냐에 따라서 다음으로 나뉨.

2.1 기본 데이터는 DB에 올리지 않고(클라이언트에 저장), 획득한 캐릭터의 데이터만 저장

  • 장점
    • 저장 용량 절약(캐릭터가 많아질 수록 효율적)
    • owned 여부는 데이터 존재 여부로 판별함.
    • 보유하지 않은 캐릭터를 UI로 표시하기 위해선 기본 데이터는 클라이언트에 저장 후 불러오면 됨.
  • 단점
    • 밸런싱 등으로 캐릭터 데이터의 변동이 잦을 경우 바로 반영 안됨 - 라이브 서비스 게임의 경우 유지보수에 어려움이 있음

2.2 기본데이터와 획득한 캐릭터의 데이터 저장을 전부 DB에 올리는 경우

  • 장점

    • DB의 데이터를 수정하는 방식으로 서버 반영을 진행하여 클라 업뎃 없이 밸런스 패치 적용 가능
    • 기본 데이터의 변동이 잦을 경우 해당 방식이 유지보수성이 좋음.
  • 단점

    • 기본데이터의 데이터가 커질 경우 네트워크의 부담이 크다.
      -> 클라이언트 실행 시 기본 데이터 자체를 캐싱하여 클라이언트에 저장한 다음 필요할 때만 새로 받는 구조로 설계하는 게 좋음

2.4 번외 - 트럭 애니메이션 만들기

3. 개선점 및 과제

3.1 리서치 내용을 컨펌받고 캐릭터 정보를 DB와 연결하기

3.2 UnitData와 UnitUpgradeData 연결하기

3.3 리팩토링

3.4 UI 폴리싱

profile
게임 만들러 코딩 공부중

0개의 댓글