게임이 마우스 조작 없이 WASD/상하좌우로만 이루어질 예정이기 때문에 우선 인디케이터를 하나 추가했습니다.
UI Depth 및 Eventsystem.current.currentSelectedGameObject 추적 서비스도 만들었지만 주 내용은 인디케이터입니다.
public interface IRectView
{
public void SetPivot(Vector2 pivot);
public void SetRect(Vector2 rect);
public Vector2 GetCurrentRectSize();
public void SetPosition(Vector2 position);
public void SetAnchoredPosition(Vector2 anchoredPosition);
public Vector2 GetPosition();
public Vector2 GetAnchoredPosition();
}
우선 RectTransform을 그대로 참조하긴 좀 그러니 Rect 정보를 가져오는 View를 만들었습니다.
public interface IUIIndicatorPresenter: IUIPresenter
{
public UniTask MoveAsync(IRectView targetRect, bool isImmediately = false);
}
public class BaseUIIndicatorPresenter : IUIIndicatorPresenter
{
private readonly BaseUIIndicatorView view;
private readonly CTSContainer cts = new();
public BaseUIIndicatorPresenter(Transform root, IRectView targetRect, BaseUIIndicatorView view)
{
this.view = view;
view.SetRoot(root);
view.SetPosition(targetRect.GetPosition());
view.SetRect(targetRect.GetCurrentRectSize());
}
public async UniTask MoveAsync(IRectView targetRect, bool isImmediately = false)
{
cts.Cancel(regenerate: true); //중복 호출 시 기존 이동 멈춤
var targetPosition = targetRect.GetPosition();
var targetRectSize = targetRect.GetCurrentRectSize();
if (isImmediately) //isImmediately라면 즉시 이동
{
view.SetPosition(targetPosition);
view.SetRect(targetRectSize);
await UniTask.CompletedTask;
}
else
{
var targetDuration = GlobalManager.instance.Table.UISO.IndicatorDuration;
var time = 0.0f;
var currentPosition = view.transform.position; //위치뿐만 아니라
var currentRectsize = view.GetCurrentRectSize();//크기도 맞추기
try
{
while (time < targetDuration)
{
cts.token.ThrowIfCancellationRequested();
var t = time / targetDuration;
view.SetPosition(Vector2.Lerp(currentPosition, targetPosition, t));
view.SetRect(Vector2.Lerp(currentRectsize, targetRectSize, t));
time += Time.deltaTime;
await UniTask.Yield(PlayerLoopTiming.Update);
}
view.SetPosition(targetPosition);
view.SetRect(targetRectSize);
}
catch (OperationCanceledException) { }
}
}
//별 내용 없는 메소드 생략
}
Indicator에는 IRectView를 받으면 해당 위치로 이동, Indicator의 크기까지 맞춰서 매끄럽게 이동하는 것처럼 보여줍니다.
그럼 MoveAsync는 어디서 호출하느냐?
public interface IUIIndicatorService //Indicator 관리 인터페이스
{
public UniTask<IUIIndicatorPresenter> CreateAsync(Transform root, IRectView beginTarget);
//생성
public IUIIndicatorPresenter GetCurrent();
//뎁스에 따라 쌓인 Indicator Stack에서 현제 최상위 뎁스의 Indicator 가져오기
public bool TryGetCurrent(out IUIIndicatorPresenter current);
//TryGet 버전
public void AttachCurrentWithGameObject(GameObject target);
//최상위 Indicator랑 GameObject를 연결해 동시에 파괴되도록
public void Push(IUIIndicatorPresenter presenter);
public void Pop();
//Indicator 생성할 때마다 수동으로 Push/Pop 하기
}
우선 Indicator 관리용 Service입니다.
UIManager에 박아뒀습니다.
public interface IUISelectionEventService
//EventSystem.current.currentSelectedGameObject 관리용
{
public enum EventType
{
OnEnter,
OnExit,
}
public void SetSelectedObject(GameObject gameObject);
public void SubscribeEvent(EventType type, UnityAction<IRectView> action);
public void UnsubscribeEvent(EventType type, UnityAction<IRectView> action);
}
public class UISelectionService : IUISelectionEventService
{
private UnityEvent<IRectView> onSelectEnter = new();
private UnityEvent<IRectView> onSelectExit = new();
private GameObject previousSelectedObject;
public void UpdateDetectingSelectedObject()
{
//대충 EventSystem.current.currentSelectedGameObject 변경될 때마다
//이전 IRectView는 OnExit, 신규 IRectView는 OnEnter를 발동합니다
var currentSelectedObject = EventSystem.current.currentSelectedGameObject;
if(previousSelectedObject != null &&
currentSelectedObject != null &&
currentSelectedObject != previousSelectedObject)
{
onSelectExit?.Invoke(previousSelectedObject.GetComponent<IRectView>());
onSelectEnter?.Invoke(currentSelectedObject.GetComponent<IRectView>());
}
else if(previousSelectedObject != null &&
currentSelectedObject == null)
{
onSelectExit?.Invoke(previousSelectedObject.GetComponent<IRectView>());
}
else if(previousSelectedObject == null &&
currentSelectedObject != null)
{
onSelectEnter?.Invoke(currentSelectedObject.GetComponent<IRectView>());
}
previousSelectedObject = currentSelectedObject;
}
public void SetSelectedObject(GameObject gameObject)
=> EventSystem.current.SetSelectedGameObject(gameObject);
public void SubscribeEvent(IUISelectionEventService.EventType type, UnityAction<IRectView> action)
//생략
public void UnsubscribeEvent(IUISelectionEventService.EventType type, UnityAction<IRectView> action)
//생략
}
현재 선택중인 UI 관련해 이벤트를 호출해줄 SelectionEventService입니다.
인자를 IRectView로 했는데 생각해보니까 이건 아무래도 GameObject로 수정을 해야 할지도 모르겠네요.
암튼 이 두개를 이용해
public class SomethingClass
{
private void SomeMethod()
{
IUIIndicatorService indicatorService = GlobalManager.instance.UIManager;
indicator = await indicatorService.CreateAsync(indicatorRoot, firstRect);
indicatorService.Push(indicator);
indicatorService.AttachCurrentWithGameObject(gameObject);
//신규 뎁스의 Root UI에서 Indicator 생성,
//IndicatorService.Push로 현재 뎁스의 Indicator 설정
IUISelectionEventService selectionEventService = GlobalManager.instance.UIManager;
selectionEventService.SubscribeEvent(IUISelectionEventService.EventType.OnEnter, OnSelectEnter);
//EventSystem.current.currentSelectedGameObject가 변경될 때마다
//indicator가 해당 오브젝트를 따라가도록 이벤트를 구독
}
private void OnSelectEnter(IRectView rectView)
{
indicator.MoveAsync(rectView).Forget();
}
//잊지 말고 OnDestroy나 DisPose에서 이벤트 해제하기
}

다이어그램 문법은 안 맞는 것 같지만 대충 IndicatorService를 통해 Indicator 가져오기,
SelectionEventService를 통해 현재 선택중인 UI 추적 이렇게 이루어집니다.
동적 UI 생성인데 Navigation은 어떻게 하였느냐?
public interface INavigationView
{
public Selectable GetSelectable();
public void AddNavigation(Direction direction, Selectable target);
public void SetNavigation(Selectable up, Selectable right, Selectable down, Selectable left);
}
public class BaseNavigationView : MonoBehaviour, INavigationView
{
[SerializeField] private Selectable selectable;
private void Awake()
{
if (selectable == null)
throw new System.NotImplementedException($"{gameObject.name}에 selectable 할당 않 됢!");
}
public Selectable GetSelectable()
=> selectable;
public void SetNavigation(Selectable up, Selectable right, Selectable down, Selectable left)
{
var navigation = new Navigation();
navigation.mode = Navigation.Mode.Explicit;
navigation.selectOnUp = up;
navigation.selectOnRight = right;
navigation.selectOnDown = down;
navigation.selectOnLeft = left;
selectable.navigation = navigation;
}
public void AddNavigation(Direction direction, Selectable target)
{
var navigation = GetSelectable().navigation;
navigation.mode = Navigation.Mode.Explicit;
switch (direction)
{
case Direction.Up:
navigation.selectOnUp = target;
break;
case Direction.Down:
navigation.selectOnDown = target;
break;
case Direction.Left:
navigation.selectOnLeft = target;
break;
case Direction.Right:
navigation.selectOnRight = target;
break;
default: throw new System.NotImplementedException();
}
selectable.navigation = navigation;
}
}
아시다시피 Unity Selectable 기본 Navigation을 자동으로 두면 아주그냥 패널을 넘어서 화면에 보이는 놈들이 죄다 연결되지 않습니까.
스크립트를 통해 직접 꽂아넣는 방법밖에 떠오르는게 없었습니다.
//중략
var row = Random.Range(2, 5);
var column = Random.Range(2, 5);
var navigations = new BaseNavigationView[row, column];
//중략, 버튼 생성하면서 해당 버튼.GetComponent<BaseNavigationView>를 저장
for (int i = 0; i < row; i++)
{
for (int j = 0; j < column; j++)
{
Set(Direction.Up, x: j, y: i + 1);
Set(Direction.Right, x: j + 1, y: i);
Set(Direction.Down, x: j, y: i - 1);
Set(Direction.Left, x: j - 1, y: i);
//상하좌우 인덱스별로 직접 할당
void Set(Direction direction, int x, int y)
{
if (x < 0 || x == column || y < 0 || y == row)
return;
navigations[i, j].AddNavigation(direction, navigations[y, x].GetSelectable());
}
}
}
아바바바밧
그래도 해보니까 할만 하더랍니다.
다음 목표:
뎁스까지 테스트해보기
UI WASD 이동 외 마우스 상하좌우로 클릭 구현하기(꾹 눌러서 활성화하는 식으로)
게임 내부 일시정지 만들기
이것도 잊었는데, 위 동영상은 키보드 상하좌우가 아니라 WASD로 조작한 화면입니다.

유니티 기본 ui InputActionMap에서 Navigate 항목의 InputAction들이 WASD에 더불어 키보드 상하좌우가 있었는데,
상하좌우 InputAction들을 삭제했습니다.
어짜피 상하좌우 InputAction은 Navigate 이동으로 사용할 예정이 아니라 이렇게 했습니다.
'무제_LR' 카테고리의 다른 글
| Progress UI - Stage UI에 적용 (0) | 2025.12.04 |
|---|---|
| 마우스 없이 키보드로만 조작하는 Progress 기반 UI 제작 (0) | 2025.12.03 |
| 피격 후 무적 시간 적용 (0) | 2025.11.22 |
| 플레이어에 FSM 적용에 FSM 적용 (0) | 2025.11.21 |
| 간단한 연출용 UI(Animator 사용) (0) | 2025.11.18 |