223 lines
7.7 KiB
C#
223 lines
7.7 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
|
|
/// <summary>
|
|
/// 캐릭터를 우클릭하면 뜨는 포즈 목록. 항목을 누르면 그 포즈가 된다.
|
|
///
|
|
/// 이전에는 우클릭할 때마다 다음 포즈로 넘어갔는데, 그러면 어떤 포즈가 있는지
|
|
/// 보이지 않고 원하는 것까지 여러 번 눌러야 한다. 무엇보다 목록에 없는 포즈는
|
|
/// 도달할 방법이 아예 없어서(예: 가림 포즈만 따로 설정한 경우) 조용히 안 되는
|
|
/// 상황이 생긴다. 목록을 눈에 보이게 하면 그 문제가 사라진다.
|
|
///
|
|
/// 채팅창과 같은 방식으로 만든다 — 런타임 uGUI, 클릭 통과 히트테스트에 사각형 등록.
|
|
/// 등록하지 않으면 클릭이 뒤 창으로 새어나가 버튼이 눌리지 않는다.
|
|
///
|
|
/// 키보드 포커스는 필요 없다(마우스만 쓴다). 그래서 채팅창과 달리
|
|
/// WS_EX_NOACTIVATE 를 건드리지 않는다.
|
|
/// </summary>
|
|
public class PoseMenuUI : MonoBehaviour
|
|
{
|
|
[Header("모양")]
|
|
[SerializeField] float menuWidth = 132f;
|
|
[SerializeField] float itemHeight = 32f;
|
|
[SerializeField] float padding = 6f;
|
|
[SerializeField] int fontSize = 13;
|
|
|
|
[Header("참조 (비우면 씬에서 탐색)")]
|
|
[SerializeField] ClickThroughHitTest hitTest;
|
|
|
|
Canvas canvas;
|
|
RectTransform panel;
|
|
RectTransform itemRoot;
|
|
Action<PoseOption> onPick;
|
|
Func<Vector2, bool> hitRegion;
|
|
bool prevLeftDown;
|
|
|
|
public bool IsOpen => panel != null && panel.gameObject.activeSelf;
|
|
|
|
void Awake()
|
|
{
|
|
if (hitTest == null) hitTest = FindFirstObjectByType<ClickThroughHitTest>();
|
|
|
|
ChatUiBuilder.EnsureEventSystem();
|
|
Build();
|
|
panel.gameObject.SetActive(false);
|
|
}
|
|
|
|
void OnEnable()
|
|
{
|
|
if (hitTest != null)
|
|
{
|
|
hitRegion = ContainsScreenPoint;
|
|
hitTest.RegisterInteractiveRegion(hitRegion);
|
|
}
|
|
}
|
|
|
|
void OnDisable()
|
|
{
|
|
if (hitTest != null && hitRegion != null)
|
|
{
|
|
hitTest.UnregisterInteractiveRegion(hitRegion);
|
|
hitRegion = null;
|
|
}
|
|
Hide();
|
|
}
|
|
|
|
void Update()
|
|
{
|
|
if (!IsOpen) return;
|
|
|
|
// 바깥을 누르면 닫는다. 전체 화면 배경판을 깔면 화면 전체가 클릭을 받아버려서
|
|
// 데스크톱 오버레이로서는 쓸 수 없다. 그래서 직접 커서를 확인한다.
|
|
#if !UNITY_EDITOR && UNITY_STANDALONE_WIN
|
|
bool leftDown = Win32.IsKeyDown(Win32.VK_LBUTTON);
|
|
#else
|
|
var mouse = UnityEngine.InputSystem.Mouse.current;
|
|
bool leftDown = mouse != null && mouse.leftButton.isPressed;
|
|
#endif
|
|
|
|
if (leftDown && !prevLeftDown)
|
|
{
|
|
if (!DesktopCursor.TryGetScreenPosition(CursorHwnd, out Vector2 cursor) ||
|
|
!ContainsScreenPoint(cursor))
|
|
{
|
|
Hide();
|
|
}
|
|
}
|
|
prevLeftDown = leftDown;
|
|
|
|
var keyboard = UnityEngine.InputSystem.Keyboard.current;
|
|
if (keyboard != null && keyboard.escapeKey.wasPressedThisFrame) Hide();
|
|
}
|
|
|
|
IntPtr CursorHwnd
|
|
{
|
|
get
|
|
{
|
|
var window = hitTest != null ? hitTest.GetComponent<TransparentWindow>() : null;
|
|
return window != null ? window.Hwnd : IntPtr.Zero;
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------ 조작
|
|
|
|
/// <summary>커서 위치에 목록을 띄운다. 이미 열려 있으면 다시 그린다.</summary>
|
|
public void Show(Vector2 screenPos, IReadOnlyList<PoseOption> poses,
|
|
Action<PoseOption> pick)
|
|
{
|
|
if (panel == null || poses == null || poses.Count == 0) return;
|
|
|
|
onPick = pick;
|
|
Rebuild(poses);
|
|
|
|
panel.gameObject.SetActive(true);
|
|
Place(screenPos);
|
|
|
|
// 이번 프레임의 우클릭이 "바깥 클릭"으로 오인되지 않게 한 번 흡수한다.
|
|
prevLeftDown = true;
|
|
}
|
|
|
|
public void Hide()
|
|
{
|
|
if (panel != null) panel.gameObject.SetActive(false);
|
|
onPick = null;
|
|
}
|
|
|
|
public bool ContainsScreenPoint(Vector2 screenPoint)
|
|
{
|
|
if (!IsOpen || panel == null) return false;
|
|
return RectTransformUtility.RectangleContainsScreenPoint(panel, screenPoint, null);
|
|
}
|
|
|
|
void Place(Vector2 screenPos)
|
|
{
|
|
float scale = canvas != null ? canvas.scaleFactor : 1f;
|
|
Vector2 size = panel.sizeDelta * scale;
|
|
|
|
// 커서 오른쪽 아래에 띄우되 화면 밖으로 나가지 않게 한다.
|
|
float x = Mathf.Clamp(screenPos.x, 0f, Mathf.Max(0f, Screen.width - size.x));
|
|
float y = Mathf.Clamp(screenPos.y - size.y, 0f, Mathf.Max(0f, Screen.height - size.y));
|
|
|
|
panel.anchoredPosition = new Vector2(x, y) / scale;
|
|
}
|
|
|
|
// ------------------------------------------------------------------ 계층
|
|
|
|
void Build()
|
|
{
|
|
var canvasGo = new GameObject("PoseMenuCanvas", typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster));
|
|
canvasGo.transform.SetParent(transform, false);
|
|
|
|
canvas = canvasGo.GetComponent<Canvas>();
|
|
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
|
canvas.sortingOrder = 210; // 채팅창(200) 위에
|
|
|
|
var scaler = canvasGo.GetComponent<CanvasScaler>();
|
|
scaler.uiScaleMode = CanvasScaler.ScaleMode.ConstantPixelSize;
|
|
scaler.scaleFactor = 1f;
|
|
|
|
panel = ChatUiBuilder.NewRect("Panel", canvasGo.transform as RectTransform);
|
|
panel.anchorMin = Vector2.zero;
|
|
panel.anchorMax = Vector2.zero;
|
|
panel.pivot = Vector2.zero;
|
|
|
|
var background = panel.gameObject.AddComponent<Image>();
|
|
background.sprite = ChatUiTheme.RoundedSmall;
|
|
background.type = Image.Type.Sliced;
|
|
background.color = new Color(
|
|
ChatUiTheme.PanelBackground.r, ChatUiTheme.PanelBackground.g,
|
|
ChatUiTheme.PanelBackground.b, 0.97f);
|
|
|
|
itemRoot = ChatUiBuilder.NewRect("Items", panel);
|
|
ChatUiBuilder.Stretch(itemRoot, padding, padding);
|
|
var layout = ChatUiBuilder.MakeVerticalList(itemRoot, new RectOffset(0, 0, 0, 0), 2f);
|
|
layout.childForceExpandHeight = false;
|
|
}
|
|
|
|
void Rebuild(IReadOnlyList<PoseOption> poses)
|
|
{
|
|
for (int i = itemRoot.childCount - 1; i >= 0; i--)
|
|
{
|
|
Destroy(itemRoot.GetChild(i).gameObject);
|
|
}
|
|
|
|
for (int i = 0; i < poses.Count; i++)
|
|
{
|
|
PoseOption pose = poses[i];
|
|
string label = string.IsNullOrWhiteSpace(pose.label) ? LabelFor(pose.kind) : pose.label;
|
|
|
|
var button = ChatUiBuilder.NewButton(pose.kind.ToString(), itemRoot, label, fontSize,
|
|
ChatUiTheme.InputBackground, ChatUiTheme.PrimaryText);
|
|
ChatUiBuilder.SetHeight(button.transform, itemHeight);
|
|
|
|
button.onClick.AddListener(() =>
|
|
{
|
|
var callback = onPick;
|
|
Hide();
|
|
callback?.Invoke(pose);
|
|
});
|
|
}
|
|
|
|
panel.sizeDelta = new Vector2(
|
|
menuWidth,
|
|
poses.Count * itemHeight + Mathf.Max(0, poses.Count - 1) * 2f + padding * 2f);
|
|
}
|
|
|
|
/// <summary>목록에 보여줄 이름. 열거형 이름을 그대로 쓰면 사용자에게 불친절하다.</summary>
|
|
static string LabelFor(CharacterMotionKind kind)
|
|
{
|
|
switch (kind)
|
|
{
|
|
case CharacterMotionKind.Idle: return "서기";
|
|
case CharacterMotionKind.Sit: return "걸터앉기";
|
|
case CharacterMotionKind.LieDown: return "눕기";
|
|
case CharacterMotionKind.Walk: return "걷기";
|
|
case CharacterMotionKind.Fall: return "낙하";
|
|
case CharacterMotionKind.Jump: return "점프";
|
|
default: return kind.ToString();
|
|
}
|
|
}
|
|
}
|