본문 바로가기

무제_LR

Addressable 구조 구현, 테이블 적용

public class GameManager : MonoBehaviour
{
  public static GameManager instance;

  private FactoryManager factoryManager;
  public FactoryManager FactoryManager => factoryManager;

  [SerializeField] private TableContainer table;
  public TableContainer Table => table;

  private ResourceManager resourceManager;
  public ResourceManager ResourceManager => resourceManager;

  private CompositeDisposable disposables = new();

  [SerializeField] private SceneProvider sceneProvider;
  public SceneProvider SceneProvider => sceneProvider;

  private void Awake()
  {
    if (instance == null)
    {
      instance = this;
      DontDestroyOnLoad(gameObject);

      factoryManager = new FactoryManager();
      factoryManager.Initialize();

      resourceManager = new ResourceManager();
      disposables.Add(resourceManager);
    }
      else
      Destroy(gameObject);    
  }

  private void OnDestroy()
  {
    disposables.Dispose();
  }
}

어드레서블과 테이블 구조를 적용했습니다.

글 제목과 위 문장에서 기묘한 기시감이 느껴진다면 기분탓입니다.

Addressable로 로드, 오브젝트 생성을 담당하는 ResourceManager,

각종 테이블을 저장하는 TableContainer,

씬 관련 로직을 담당하는 SceneProvider를 추가했습니다.

이름에 대해서는 채찍PT한테 만번 물어봐야 하나 고민하고 있긴 합니다.

 

public class ResourceManager : IDisposable
{
  readonly private Dictionary<IResourceLocation, AsyncOperationHandle> loadHandles = new();
  readonly private List<AsyncOperationHandle> createdHandles = new();

  public async UniTask LoadAssetsAsync(string key)
  {
    var locations = await GetLocationsAsync(key);
    await LoadAssetsAsync(locations);
  }

  public async UniTask LoadAssetsAsync(AssetReference assetReference)
  {
    var locations = await GetLocationsAsync(assetReference);
    await LoadAssetsAsync(locations);
  }

  public async UniTask LoadAssetsAsync(IList<IResourceLocation> locations)
  {
    var handles = new List<AsyncOperationHandle>();
    foreach (var location in locations)
    {
      var loadHandle = await LoadAsync(location);
      handles.Add(loadHandle);
    }

    await Addressables.ResourceManager.CreateGenericGroupOperation(handles, true);
  }

  public async UniTask<AsyncOperationHandle> LoadAsync(IResourceLocation location)
  {
    var handle = Addressables.LoadAssetAsync<object>(location);
    await handle;
    loadHandles[location] = handle;
    return handle;
  }

  public async UniTask<T> CreateAssetAsync<T>(string key, Transform root = null) where T : MonoBehaviour
  {
    var results = await CreateAssetsAsync<T>(key, root);
    return results.First();
  }

  public async UniTask<T> CreateAssetAsync<T>(AssetReference assetReference, Transform root = null) where T : MonoBehaviour
  {
    var results = await CreateAssetsAsync<T>(assetReference, root);
    return results.First();
  }

  public async UniTask<List<T>> CreateAssetsAsync<T>(string key, Transform root = null) where T : MonoBehaviour
  {
    var locations = await GetLocationsAsync(key);
    return await CreateAssetsAsync<T>(locations, root);
  }

  public async UniTask<List<T>> CreateAssetsAsync<T>(AssetReference assetReference, Transform root = null) where T: MonoBehaviour
  {
    var locations = await GetLocationsAsync(assetReference);
    return await CreateAssetsAsync<T>(locations, root);
  }

  private async UniTask<List<T>> CreateAssetsAsync<T>(IList<IResourceLocation> locations, Transform root = null) where T:MonoBehaviour
  {
    var objects = new List<T>();
    foreach (var location in locations)
    {
      if (!loadHandles.TryGetValue(location, out var handle))
        handle = await LoadAsync(location);

      var createdGameObject = MonoBehaviour.Instantiate(handle.Result as GameObject, root);
      createdHandles.Add(handle);

      objects.Add(createdGameObject.GetComponent<T>());
    }

    return objects;
  }

  private async UniTask<IList<IResourceLocation>> GetLocationsAsync(string key)
  {
    var locationHandle = Addressables.LoadResourceLocationsAsync(key);
    await locationHandle;
    return locationHandle.Result;
  }

  private async UniTask<IList<IResourceLocation>> GetLocationsAsync(AssetReference assetReference)
  {
    var locationHandle = Addressables.LoadResourceLocationsAsync(assetReference);
    await locationHandle;
    return locationHandle.Result;
  }

  public void ReleaseAll()
  {
    foreach (var handle in loadHandles.Values)
      handle.Release();
    loadHandles.Clear();
  }

  public void Dispose()
  {
    ReleaseAll();
  }
}

각 메서드 본문은 생략할까 했다만 그냥 다 올렸습니다.

Key->Location->Handle과정을 거치도록 path혹은 label, AssetReference를 통해 로드할 때마다 전부 Location으로 변환해준 후 내부에 로드된 에셋들을 기록합니다.

CreateAssetAsync는 게임오브젝트로 생성해야 하는 상황일테니 리턴값을 제네릭으로 하되 MonoBehaviour로 조건을 걸었습니다.

 

public class TableContainer : MonoBehaviour
{
  [SerializeField] private PlayerModelSO leftPlayerModelSO;
  public PlayerModelSO LeftPlayerModelSO=>leftPlayerModelSO;

  [SerializeField] private PlayerModelSO rightPlayerModelSO;
  public PlayerModelSO RightPlayerModelSO =>rightPlayerModelSO;

  [SerializeField] private AddressableKeySO addressableKeySO;
  public AddressableKeySO AddressableKeySO => addressableKeySO;
}

TableSO는 보이는 대로 정보 가져오는 테이블만.

 

public enum SceneType
{
  Initialize,
  Game,
}

public class SceneProvider : MonoBehaviour
{
  [SerializeField] private AssetReference gameSceneReference;

  public async UniTask LoadSceneAsync(SceneType sceneType, LoadSceneMode loadSceneMode = LoadSceneMode.Single, UnityAction<float> onProgress = null, Func<UniTask> waitUntilLoad=null)
  {
    var sceneRefernece = ParseSceneReference(sceneType);
    var handle = Addressables.LoadSceneAsync(sceneRefernece,loadSceneMode,false);
    while (!handle.IsDone)
    {
      onProgress?.Invoke(handle.PercentComplete);
      await UniTask.Yield(PlayerLoopTiming.Update);
    }
    onProgress?.Invoke(1.0f);

    if (waitUntilLoad != null)
      await waitUntilLoad.Invoke();

    await handle.Result.ActivateAsync();
  }

  private AssetReference ParseSceneReference(SceneType sceneType)
    => sceneType switch
    {
      SceneType.Initialize => throw new System.NotImplementedException(),
      SceneType.Game => gameSceneReference,
      _ => throw new System.NotImplementedException(),
    };
}

SceneProvider에서 씬을 불러오는 과정 역시 Addresable을 사용하는지라 이 로직도 ResourceManager에 위임해야 할까 싶었는데 이정도는 괜찮지 않을까 했습니다.

로딩 UI를 위한 onProgress, 씬 로딩 후 클릭 혹은 버튼 입력 후 씬을 넘어가는 등 기능을 위해 Func<Task> waitUntilLoad 인자도 만들었습니다.

올바른 작명법인지는 몰루

 

public class PlayerSetupService : MonoBehaviour, IStageObjectSetupService<BasePlayerPresenter>
{
  private IPlayerPresenter leftPlayer;
  private IPlayerPresenter rightPlayer;

  public async UniTask SetupAsync()
  {
    leftPlayer = await CreateLeftPlayer();
    rightPlayer = await CreateRighPlayer();

    leftPlayer.EnableAllInputActions(true);
    rightPlayer.EnableAllInputActions(true);
  }

  public void Release()
  {
    throw new System.NotImplementedException();
  }

  private async UniTask<IPlayerPresenter> CreateLeftPlayer()
  {
    var leftPlayerKey = GameManager.instance.Table.AddressableKeySO.LeftPlayer;	//←여기바뀜
    var leftView = await GameManager.instance.ResourceManager.CreateAssetAsync<BasePlayerView>(leftPlayerKey);
    //↑여기바뀜
    var model = new PlayerModel(Vector3.up,Vector3.down,Vector3.left,Vector3.right);
    var presenter = new BasePlayerPresenter();
    presenter.Initialize(leftView, model);

    var modelSO = GameManager.instance.Table.LeftPlayerModelSO;
    presenter.CreateMoveInputAction(InputActionPaths.ParshPath(modelSO.UpKeyCode), Direction.Up);
    presenter.CreateMoveInputAction(InputActionPaths.ParshPath(modelSO.RightKeyCode), Direction.Right);
    presenter.CreateMoveInputAction(InputActionPaths.ParshPath(modelSO.DownKeyCode), Direction.Down);
    presenter.CreateMoveInputAction(InputActionPaths.ParshPath(modelSO.LeftKeyCode), Direction.Left);

    await UniTask.CompletedTask;
    return presenter;
  }

  private async UniTask<IPlayerPresenter> CreateRighPlayer()
  {
    var rightPlayerKey = GameManager.instance.Table.AddressableKeySO.RightPlayer;//←여기바뀜
    var rightView = await GameManager.instance.ResourceManager.CreateAssetAsync<BasePlayerView>(rightPlayerKey);
    //↑여기바뀜
    var model = new PlayerModel(Vector3.up, Vector3.down, Vector3.left, Vector3.right);
    var presenter = new BasePlayerPresenter();
    presenter.Initialize(rightView, model);

    var modelSO = GameManager.instance.Table.RightPlayerModelSO;
    presenter.CreateMoveInputAction(InputActionPaths.ParshPath(modelSO.UpKeyCode), Direction.Up);
    presenter.CreateMoveInputAction(InputActionPaths.ParshPath(modelSO.RightKeyCode), Direction.Right);
    presenter.CreateMoveInputAction(InputActionPaths.ParshPath(modelSO.DownKeyCode), Direction.Down);
    presenter.CreateMoveInputAction(InputActionPaths.ParshPath(modelSO.LeftKeyCode), Direction.Left);

    await UniTask.CompletedTask;
    return presenter;
  }
}

어드레서블을 사용한 리소스 로드 방식으로 바뀌어 기존에 씬에 존재하던 오브젝트를 참조해 이니셜라이징 시키던 PlayerSetupService가 동적으로 오브젝트를 불러와 생성하도록 바뀌었습니다.

CreateMoveInputAction의 상하좌우 방향키도 테이블 SO에서 받아오도록 수정됐습니다.

싱글톤인 게임매니저, 이후 기타 등등 프리로드 에셋이라던가 게임 시작 전 최초로 이니셜라이징을 실행할 Initialize 씬을 추가, 즉 씬 플로우는 Initialzie -> Game로 넘어가도록 되어 있습니다.

 

public class InitializeSceneUI : MonoBehaviour
{
  [SerializeField] private Button startButton;

  private readonly CompositeDisposable disposables = new();

  private void Awake()
  {
    startButton
      .OnClickAsObservable()
      .Take(1)
      .Subscribe(_ => ChangeToGameScene());
  }

  private void ChangeToGameScene()
  {
    GameManager.instance.SceneProvider.LoadSceneAsync(
      SceneType.Game,
      UnityEngine.SceneManagement.LoadSceneMode.Single,
      onProgress: null,
      waitUntilLoad: null).Forget();
  }
}

UI 버튼은 UniRx를 통해 버튼을 누르면 1회 한정해 씬을 넘기도록 설정했습니다.

 

Game 씬으로 변경되면 GameManger는 전역 싱글톤이기 때문에 파괴될 것이고

LocalManager의 StageManager가 플레이어를 생성 이후 이전에 올렸던 것처럼 초기화해줍니다.

 

다음 목표:

타일맵으로 맵 생성

클리어 트리거 추가

적대적 오브젝트 생성

스테이지 단위 로드 및 생성

승리/패배 로직

재시작 추가

 

생각보다 할 게 많아졌네요.

'무제_LR' 카테고리의 다른 글

스테이지 생성(트리거 타일)  (0) 2025.11.06
스테이지 생성(타일맵)  (0) 2025.11.06
스테이지 데이터 구조  (0) 2025.11.03
플레이어 MVP  (0) 2025.11.01
첫 기획 - 구조 잡아보기  (0) 2025.10.30