본문 바로가기

무제_LR

간단한 연출용 UI(Animator 사용)

 

눈에 띄게 달라진건 없고 적당히 입력 연출용 UI를 추가했읍니다.

 

적당히 Animator로 플레이어 입력에 따라 Bool 값을 변경하도록 했습니다.

 

BaseAnimatorView는 아래 IAnimatorView를 그대로 구현하는 클래스입니다.

생각해보니까 지금까지 View라고 붙여야 할 것들을 죄다 Controller라고 이름붙였더라구요.

  public interface IAnimatorView
  {
    public void SetBool(int hashID, bool value);

    public void SetFloat(int hashID, float value);

    public void SetInt(int hashID, int value);

    public void SetTrigger(int hashID);

    public void Play(int hashID);
  }
  [RequireComponent(typeof(Animator))]
  public class BaseAnimatorView : MonoBehaviour, IAnimatorView
  {
    private Animator animator;

    private void OnEnable()
    {
      animator = GetComponent<Animator>();
    }

    public void Play(int hashID)
        => animator.Play(hashID);

    public void SetBool(int hashID, bool value)
      => animator.SetBool(hashID, value);

    public void SetFloat(int hashID, float value)
      => animator.SetFloat(hashID, value);

    public void SetInt(int hashID, int value)
      => animator.SetInteger(hashID, value);

    public void SetTrigger(int hashID)
      => animator.SetTrigger(hashID);
  }

더도말고 덜도말고 인터페이스 하나만 사용할 View는 해당 IView만 구현해놓은 BaseView를 사용하도록 할 예정입니다.

  public class UIPlayerInputViewContainer : MonoBehaviour
  {
    public BaseAnimatorView upView;
    public BaseAnimatorView downView;
    public BaseAnimatorView leftView;
    public BaseAnimatorView rightView;
  }

그리하여 굳이 UIPlayerInputAnimatorView를 구현할 필요 없이 적당한 BaseAnimatorView를 가져다 쓰게 하니 훨씬 편해졌습니다.

종류별로 BaseView를 만들어뒀습니다.

언젠가 여러 종류 컴포넌트를 복합적으로 사용해야 할 경우가 생기겠지만 그건 그때 가서 새로 클래스를 만들면 될 일이지요.

 

  public class UIPlayerInputPresenter : IUIPresenter
  {
    public class Model
    {

    }

    private readonly Model model;
    private readonly UIPlayerInputViewContainer leftViewContainer;
    private readonly UIPlayerInputViewContainer rightViewContainer;
    //string 대신 int값을 전달하도록 미리 해시값을 가져다놓습니다.
    //생각해보니까 이건 Model에 들어가야 했을 것 같네요.
    private readonly int activeHash = Animator.StringToHash("Active");

    public UIPlayerInputPresenter(
      Model model,
      UIPlayerInputViewContainer leftViewContainer,
      IPlayerMoveSubscriber leftSubscriber, //IPlayerMoveSubscriber: 새로 추가한 인터페이스
      UIPlayerInputViewContainer rightViewContainer,
      IPlayerMoveSubscriber rightSubscriber)//플레이어 이동 인풋의 Performed, 
    {										//Cancled에 이벤트를 넣을 수 있게 했습니다.
      this.model = model;
      this.leftViewContainer = leftViewContainer;
      this.rightViewContainer = rightViewContainer;

		//left 플레이어의 이동 InputAction Performed에 이벤트를 등록
      leftSubscriber.SubscribeOnPerformed(direction=>
      {	//여기서 direction은 Up,Left,Down,Right로 구성된 Enum입니다.
        var view = direction switch
        {	//방향에 따라서 View Animator의
          Direction.Up => leftViewContainer.upView,
          Direction.Down => leftViewContainer.downView,
          Direction.Left => leftViewContainer.leftView,
          Direction.Right => leftViewContainer.rightView,
          _=>throw new System.NotImplementedException(),
        };	//Bool 값을 변경해 애니메이션이 실행되도록 합니다.
        view.SetBool(activeHash, true);
      });
      //버튼을 땠을 때는 원래도록 돌아와야 하니
      leftSubscriber.SubscribeOnCanceled(direction =>
      {
        var view = direction switch
        {
          Direction.Up => leftViewContainer.upView,
          Direction.Down => leftViewContainer.downView,
          Direction.Left => leftViewContainer.leftView,
          Direction.Right => leftViewContainer.rightView,
          _ => throw new System.NotImplementedException(),
        };
        view.SetBool(activeHash, false);	//Canceled에도 실행되도록 등록해줍니다.
      });
      //right에도 비슷하게 적용
      rightSubscriber.SubscribeOnPerformed(direction =>
      {
        var view = direction switch
        {
          Direction.Up => rightViewContainer.upView,
          Direction.Down => rightViewContainer.downView,
          Direction.Left => rightViewContainer.leftView,
          Direction.Right => rightViewContainer.rightView,
          _ => throw new System.NotImplementedException(),
        };
        view.SetBool(activeHash, true);
      });
      rightSubscriber.SubscribeOnCanceled(direction =>
      {
        var view = direction switch
        {
          Direction.Up => rightViewContainer.upView,
          Direction.Down => rightViewContainer.downView,
          Direction.Left => rightViewContainer.leftView,
          Direction.Right => rightViewContainer.rightView,
          _ => throw new System.NotImplementedException(),
        };
        view.SetBool(activeHash, false);
      });
    }

    public UniTask ShowAsync(bool isImmediately = false, CancellationToken token = default)
    {
      return UniTask.CompletedTask;
    }

    public UniTask HideAsync(bool isImmediately = false, CancellationToken token = default)
    {
      return UniTask.CompletedTask;
    }

    public IDisposable AttachOnDestroy(GameObject target)
      => target.OnDestroyAsObservable().Subscribe(_ => Dispose());

    public UIVisibleState GetVisibleState()
    {
      return UIVisibleState.Showed;
    }

    public void SetVisibleState(UIVisibleState visibleState)
    {
      throw new NotImplementedException();
    }

    public void Dispose()
    {
      IUIPresenterContainer container = GlobalManager.instance.UIManager;
      container.Remove(this);
    }
  }

 

플레이어 Presenter에 이벤트 구독용 인터페이스를 추가하기도 했습니다.

public interface IPlayerMoveSubscriber
{
  public void SubscribeOnPerformed(UnityAction<Direction> performed);

  public void SubscribeOnCanceled(UnityAction<Direction> canceled);

  public void UnsubscribePerfoemd(UnityAction<Direction> perfoemd);

  public void UnsubscribeCanceled(UnityAction<Direction> canceled);
}
public interface IPlayerPresenter: IPlayerMoveController, IPlayerMoveSubscriber, IStageObjectController

public class BasePlayerPresenter : IPlayerPresenter
{
  //생략
  private UnityEvent<Direction> onPerformed = new();
  private UnityEvent<Direction> onCanceled = new();

      //중략: 대충 플레이어 InputAction의 로직 부분
      switch (context.phase)
      {
        case InputActionPhase.Started:
          break;

        case InputActionPhase.Performed:
          view.AddDirection(vectorDirection);
          onPerformed?.Invoke(direction);
          break;

        case InputActionPhase.Canceled:
          view.RemoveDirection(vectorDirection);
          onCanceled?.Invoke(direction);
          break;
      }
}

InputAction의 context.phase Performed/Canceled 부분마다 해당 이벤트를 발동합니다.

현재 실질적으로 Phase 사용 부분이 Performed랑 Canceled뿐이니 이 둘만 만들었습니다.

 

    private async UniTask CreateInputUIPresenterAsync()
    {
      var model = new UIPlayerInputPresenter.Model();
      var leftView = viewContainer.leftViewContainer;
      var leftSubscriber = await LocalManager.instance.StageManager.GetPresenterAsync(PlayerType.Left);
      var rightView = viewContainer.rightViewContainer;
      var rightSubscriber = await LocalManager.instance.StageManager.GetPresenterAsync(PlayerType.Right);
      IUIPresenterFactory presenterFactory = GlobalManager.instance.UIManager;
      presenterFactory.Register(() => new UIPlayerInputPresenter(model, leftView, leftSubscriber, rightView, rightSubscriber));      
      inputUIPresenter = presenterFactory.Create<UIPlayerInputPresenter>();
      inputUIPresenter.AttachOnDestroy(viewContainer.gameObject);
    }

마음에 안 드는 점은, IPlayerMoveSubscriber를 가져오는 과정에서 참조가 좀 쌥니다.

UIManager 산하에서 이루어지는 일이다보니 외부에서 Player를 가져와야 하는데, 타 싱글톤에서 가져오는 방법밖에 떠오르지가 않네요.

UI쪽은 DontDestroyGameObject가 달린 GlobalManager,

오브젝트 쪽은 씬마다 고유한 LocalManager에서 담당하고 있습니다.

그래서 UI 쪽에서 플레이어 관련 코드를 가져오려면 LocalManager.instance.StageManager 등을 참조해야 했는데 아무래도 이것보단 더 좋은 방법이 있지 않을까 싶네요.

 

다음 목표: 외부 반응으로 플레이어 오브젝트 움직이기(바운스같은거)