Add radial pose menu with hotkeys, remove C cycle hotkey
- PlayerPose: Icon (Sprite) + Title (string) UI properties - PoseMenu: modal pause overlay building a radial menu from Icon Button prefab instances; active pose disabled (hover ring marks it); click applies pose via SetPoseIndex and closes - OverviewMenu: pose button wiring with pause hand-off, Esc/Tab chain extended; hotkeys C (toggle pose menu) and V (paint mode) mirroring the buttons incl. identical gating - PlayerPoseController: C cycle hotkey and NextPose() removed Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c3d9c06118
commit
3a2e8a7057
5 files changed
+216
-23
No files matched your search
@@ -3,14 +3,12 @@ using UnityEngine;
|
|||||||
using UnityEngine.Animations;
|
using UnityEngine.Animations;
|
||||||
using UnityEngine.Playables;
|
using UnityEngine.Playables;
|
||||||
using UnityEngine.Serialization;
|
using UnityEngine.Serialization;
|
||||||
#if ENABLE_INPUT_SYSTEM
|
|
||||||
using UnityEngine.InputSystem;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Owns ALL character animation (the Animator is left empty): plays the active <see cref="PlayerPose"/>'s
|
/// Owns ALL character animation (the Animator is left empty): plays the active <see cref="PlayerPose"/>'s
|
||||||
/// idle or optional walk/run/fall clip through a two-input mixer <see cref="PlayableGraph"/> that
|
/// idle or optional walk/run/fall clip through a two-input mixer <see cref="PlayableGraph"/> that
|
||||||
/// crossfades every clip switch. One key cycles poses, sprinting resets to the default pose; each pose
|
/// crossfades every clip switch. Poses are selected via <see cref="SetPoseIndex"/> (the pose menu UI);
|
||||||
|
/// sprinting resets to the default pose; each pose
|
||||||
/// also carries its own eased CharacterController capsule and per-pose camera y-offsets.
|
/// also carries its own eased CharacterController capsule and per-pose camera y-offsets.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[RequireComponent(typeof(PlayerCharacterController))]
|
[RequireComponent(typeof(PlayerCharacterController))]
|
||||||
@@ -28,15 +26,6 @@ public class PlayerPoseController : MonoBehaviour
|
|||||||
[Min(0)]
|
[Min(0)]
|
||||||
[SerializeField] private int defaultPoseIndex = 0;
|
[SerializeField] private int defaultPoseIndex = 0;
|
||||||
|
|
||||||
[Header("Input")]
|
|
||||||
#if ENABLE_INPUT_SYSTEM
|
|
||||||
[Tooltip("Key that cycles to the next pose (wraps at the end of the list).")]
|
|
||||||
[SerializeField] private Key cycleKey = Key.C;
|
|
||||||
#else
|
|
||||||
[Tooltip("Key that cycles to the next pose (wraps at the end of the list).")]
|
|
||||||
[SerializeField] private KeyCode cycleKey = KeyCode.C;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
[Header("Transitions")]
|
[Header("Transitions")]
|
||||||
[Tooltip("Seconds to crossfade from the previous pose to a newly selected one. 0 = instant snap.")]
|
[Tooltip("Seconds to crossfade from the previous pose to a newly selected one. 0 = instant snap.")]
|
||||||
[Min(0f)]
|
[Min(0f)]
|
||||||
@@ -111,6 +100,9 @@ public class PlayerPoseController : MonoBehaviour
|
|||||||
|
|
||||||
public int CurrentIndex => _currentIndex;
|
public int CurrentIndex => _currentIndex;
|
||||||
|
|
||||||
|
/// <summary>Selectable poses, for the pose selection menu (icons/titles per entry).</summary>
|
||||||
|
public IReadOnlyList<PlayerPose> Poses => poses;
|
||||||
|
|
||||||
/// <summary>Locomotion category resolved this frame; the owner publishes it for network sync.</summary>
|
/// <summary>Locomotion category resolved this frame; the owner publishes it for network sync.</summary>
|
||||||
public Locomotion CurrentLocomotion => _currentLocomotion;
|
public Locomotion CurrentLocomotion => _currentLocomotion;
|
||||||
|
|
||||||
@@ -169,9 +161,6 @@ public class PlayerPoseController : MonoBehaviour
|
|||||||
// Local control only - a remote's pose index is pushed in via SetNetworkState.
|
// Local control only - a remote's pose index is pushed in via SetNetworkState.
|
||||||
if (!_networkControlled)
|
if (!_networkControlled)
|
||||||
{
|
{
|
||||||
if (InputUtil.GameplayKeyPressed(cycleKey))
|
|
||||||
NextPose();
|
|
||||||
|
|
||||||
// Sprinting in any state (ground, air, wall, ceiling) snaps the selection back to the default
|
// Sprinting in any state (ground, air, wall, ceiling) snaps the selection back to the default
|
||||||
// pose. Held continuously, so the reset shows up immediately, not only once back on the floor.
|
// pose. Held continuously, so the reset shows up immediately, not only once back on the floor.
|
||||||
if (_player.IsSprinting)
|
if (_player.IsSprinting)
|
||||||
@@ -234,10 +223,7 @@ public class PlayerPoseController : MonoBehaviour
|
|||||||
_capsuleStarted = false; // re-snap to the active pose's capsule if this is re-enabled
|
_capsuleStarted = false; // re-snap to the active pose's capsule if this is re-enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Advances to the next pose, wrapping at the end of the list.</summary>
|
/// <summary>Selects a pose by index, wrapping into range so a stale index never goes out of bounds.</summary>
|
||||||
public void NextPose() => SetPoseIndex(_currentIndex + 1);
|
|
||||||
|
|
||||||
/// <summary>Selects a pose by index, wrapping into range so cycling never goes out of bounds.</summary>
|
|
||||||
public void SetPoseIndex(int index) => _currentIndex = WrapIndex(index);
|
public void SetPoseIndex(int index) => _currentIndex = WrapIndex(index);
|
||||||
|
|
||||||
private int WrapIndex(int index)
|
private int WrapIndex(int index)
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ using UnityEngine.InputSystem;
|
|||||||
/// The Esc overview menu (the "Overview UI" canvas in Main). Owns ALL Escape handling (Tab acts as an
|
/// The Esc overview menu (the "Overview UI" canvas in Main). Owns ALL Escape handling (Tab acts as an
|
||||||
/// alternative to Esc): Esc toggles
|
/// alternative to Esc): Esc toggles
|
||||||
/// this panel as a modal pause overlay (gameplay suspended via <see cref="GameModeManager.SetPaused"/>);
|
/// this panel as a modal pause overlay (gameplay suspended via <see cref="GameModeManager.SetPaused"/>);
|
||||||
/// while the session browser or match results overlay is open instead, Esc closes that, and while paint
|
/// while the session browser, match results or pose menu overlay is open instead, Esc closes that, and while paint
|
||||||
/// mode is active Esc exits to the camera mode (Player Control, or First Person for the hunter). Buttons
|
/// mode is active Esc exits to the camera mode (Player Control, or First Person for the hunter). Buttons
|
||||||
/// switch game modes (via <see cref="GameModeManager.TrySwitchTo{T}"/>) or open the session browser /
|
/// switch game modes (via <see cref="GameModeManager.TrySwitchTo{T}"/>) or open the session browser /
|
||||||
/// match results overlay, and every click closes the panel. Mode buttons are only shown for modes present in the current role-scoped set
|
/// match results overlay, and every click closes the panel. Mode buttons are only shown for modes present in the current role-scoped set
|
||||||
/// (<see cref="GameModeManager.Modes"/>); the active mode's button is disabled. Attach to the
|
/// (<see cref="GameModeManager.Modes"/>); the active mode's button is disabled. Hotkeys mirror the
|
||||||
|
/// buttons: C toggles the pose menu, V switches to paint mode (E = taunt lives in GameModeManager). Attach to the
|
||||||
/// always-active canvas root; <see cref="panelRoot"/> is the toggled visual child.
|
/// always-active canvas root; <see cref="panelRoot"/> is the toggled visual child.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class OverviewMenu : MonoBehaviour
|
public class OverviewMenu : MonoBehaviour
|
||||||
@@ -29,6 +30,10 @@ public class OverviewMenu : MonoBehaviour
|
|||||||
"menu stays closed. Empty = found automatically.")]
|
"menu stays closed. Empty = found automatically.")]
|
||||||
[SerializeField] private MatchResultScreen matchResultScreen;
|
[SerializeField] private MatchResultScreen matchResultScreen;
|
||||||
|
|
||||||
|
[Tooltip("Pose selection menu binder. While its overlay is open it owns Esc (close-only) and this " +
|
||||||
|
"menu stays closed. Empty = found automatically.")]
|
||||||
|
[SerializeField] private PoseMenu poseMenu;
|
||||||
|
|
||||||
[Header("Panel")]
|
[Header("Panel")]
|
||||||
[Tooltip("Visual container toggled by Esc. Must be a CHILD of this binder's object so the binder " +
|
[Tooltip("Visual container toggled by Esc. Must be a CHILD of this binder's object so the binder " +
|
||||||
"keeps reading Esc while the panel is hidden.")]
|
"keeps reading Esc while the panel is hidden.")]
|
||||||
@@ -45,6 +50,8 @@ public class OverviewMenu : MonoBehaviour
|
|||||||
[Tooltip("Triggers the hider's manual radar ping (same as the E key) and closes the menu. Only shown " +
|
[Tooltip("Triggers the hider's manual radar ping (same as the E key) and closes the menu. Only shown " +
|
||||||
"for a hider during Seek.")]
|
"for a hider during Seek.")]
|
||||||
[SerializeField] private Button tauntButton;
|
[SerializeField] private Button tauntButton;
|
||||||
|
[Tooltip("Opens the pose selection menu. Only shown while the local player has selectable poses.")]
|
||||||
|
[SerializeField] private Button poseButton;
|
||||||
|
|
||||||
private bool _open;
|
private bool _open;
|
||||||
|
|
||||||
@@ -53,6 +60,7 @@ public class OverviewMenu : MonoBehaviour
|
|||||||
if (!gameMode) gameMode = FindFirstObjectByType<GameModeManager>();
|
if (!gameMode) gameMode = FindFirstObjectByType<GameModeManager>();
|
||||||
if (!sessionBrowser) sessionBrowser = FindFirstObjectByType<OnlineSessionBrowserUI>();
|
if (!sessionBrowser) sessionBrowser = FindFirstObjectByType<OnlineSessionBrowserUI>();
|
||||||
if (!matchResultScreen) matchResultScreen = FindFirstObjectByType<MatchResultScreen>();
|
if (!matchResultScreen) matchResultScreen = FindFirstObjectByType<MatchResultScreen>();
|
||||||
|
if (!poseMenu) poseMenu = FindFirstObjectByType<PoseMenu>();
|
||||||
if (panelRoot) panelRoot.SetActive(false);
|
if (panelRoot) panelRoot.SetActive(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +73,7 @@ public class OverviewMenu : MonoBehaviour
|
|||||||
if (firstPersonButton) firstPersonButton.onClick.AddListener(OnFirstPerson);
|
if (firstPersonButton) firstPersonButton.onClick.AddListener(OnFirstPerson);
|
||||||
if (freeCamButton) freeCamButton.onClick.AddListener(OnFreeCam);
|
if (freeCamButton) freeCamButton.onClick.AddListener(OnFreeCam);
|
||||||
if (tauntButton) tauntButton.onClick.AddListener(OnTaunt);
|
if (tauntButton) tauntButton.onClick.AddListener(OnTaunt);
|
||||||
|
if (poseButton) poseButton.onClick.AddListener(OnPose);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDisable()
|
private void OnDisable()
|
||||||
@@ -76,18 +85,20 @@ public class OverviewMenu : MonoBehaviour
|
|||||||
if (firstPersonButton) firstPersonButton.onClick.RemoveListener(OnFirstPerson);
|
if (firstPersonButton) firstPersonButton.onClick.RemoveListener(OnFirstPerson);
|
||||||
if (freeCamButton) freeCamButton.onClick.RemoveListener(OnFreeCam);
|
if (freeCamButton) freeCamButton.onClick.RemoveListener(OnFreeCam);
|
||||||
if (tauntButton) tauntButton.onClick.RemoveListener(OnTaunt);
|
if (tauntButton) tauntButton.onClick.RemoveListener(OnTaunt);
|
||||||
|
if (poseButton) poseButton.onClick.RemoveListener(OnPose);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Update()
|
private void Update()
|
||||||
{
|
{
|
||||||
bool browserOpen = sessionBrowser && sessionBrowser.IsOpen;
|
bool browserOpen = sessionBrowser && sessionBrowser.IsOpen;
|
||||||
bool resultsOpen = matchResultScreen && matchResultScreen.IsOpen;
|
bool resultsOpen = matchResultScreen && matchResultScreen.IsOpen;
|
||||||
|
bool poseOpen = poseMenu && poseMenu.IsOpen;
|
||||||
|
|
||||||
if (_open)
|
if (_open)
|
||||||
{
|
{
|
||||||
// Another overlay took over (results Resolution auto-open) -> hide this panel; the pause
|
// Another overlay took over (results Resolution auto-open) -> hide this panel; the pause
|
||||||
// is that overlay's now, so don't release it.
|
// is that overlay's now, so don't release it.
|
||||||
if (browserOpen || resultsOpen)
|
if (browserOpen || resultsOpen || poseOpen)
|
||||||
HidePanel();
|
HidePanel();
|
||||||
// An external state reset (respawn/bind, disconnect) dropped the pause underneath the
|
// An external state reset (respawn/bind, disconnect) dropped the pause underneath the
|
||||||
// open panel -> don't linger as a stale overlay over live gameplay.
|
// open panel -> don't linger as a stale overlay over live gameplay.
|
||||||
@@ -95,12 +106,16 @@ public class OverviewMenu : MonoBehaviour
|
|||||||
HidePanel();
|
HidePanel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (HandleHotkeys(browserOpen, resultsOpen, poseOpen)) return;
|
||||||
|
|
||||||
if (!EscapePressed()) return;
|
if (!EscapePressed()) return;
|
||||||
|
|
||||||
if (browserOpen)
|
if (browserOpen)
|
||||||
sessionBrowser.Close();
|
sessionBrowser.Close();
|
||||||
else if (resultsOpen)
|
else if (resultsOpen)
|
||||||
matchResultScreen.Close();
|
matchResultScreen.Close();
|
||||||
|
else if (poseOpen)
|
||||||
|
poseMenu.Close();
|
||||||
else if (_open)
|
else if (_open)
|
||||||
Close(resume: true);
|
Close(resume: true);
|
||||||
// Loading (joining a session, or the pre-match Loading barrier): the scene/avatar is mid-setup,
|
// Loading (joining a session, or the pre-match Loading barrier): the scene/avatar is mid-setup,
|
||||||
@@ -157,6 +172,13 @@ public class OverviewMenu : MonoBehaviour
|
|||||||
gameMode.SetPaused(false);
|
gameMode.SetPaused(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnPose()
|
||||||
|
{
|
||||||
|
Close(resume: false);
|
||||||
|
if (!(poseMenu && poseMenu.Open()) && gameMode)
|
||||||
|
gameMode.SetPaused(false);
|
||||||
|
}
|
||||||
|
|
||||||
private void OnTaunt()
|
private void OnTaunt()
|
||||||
{
|
{
|
||||||
Close(resume: true);
|
Close(resume: true);
|
||||||
@@ -196,6 +218,10 @@ public class OverviewMenu : MonoBehaviour
|
|||||||
tauntButton.gameObject.SetActive(gameMode.LocalPlayerIsHider && MatchManager.Instance &&
|
tauntButton.gameObject.SetActive(gameMode.LocalPlayerIsHider && MatchManager.Instance &&
|
||||||
MatchManager.Instance.LocalSubPhase == MatchManager.MatchSubPhase.Seek);
|
MatchManager.Instance.LocalSubPhase == MatchManager.MatchSubPhase.Seek);
|
||||||
|
|
||||||
|
// Pose selection needs a bound local player with poses - hide the button otherwise.
|
||||||
|
if (poseButton)
|
||||||
|
poseButton.gameObject.SetActive(poseMenu && poseMenu.CanOpen);
|
||||||
|
|
||||||
SetModeButton<PlayerControlMode>(playerControlButton, modes, active);
|
SetModeButton<PlayerControlMode>(playerControlButton, modes, active);
|
||||||
SetModeButton<PaintMode>(paintButton, modes, active);
|
SetModeButton<PaintMode>(paintButton, modes, active);
|
||||||
SetModeButton<FirstPersonMode>(firstPersonButton, modes, active);
|
SetModeButton<FirstPersonMode>(firstPersonButton, modes, active);
|
||||||
@@ -213,6 +239,38 @@ public class OverviewMenu : MonoBehaviour
|
|||||||
button.interactable = !(active is T);
|
button.interactable = !(active is T);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Hotkeys mirroring the overlay buttons: C = pose menu (toggle), V = paint mode.
|
||||||
|
/// Same gating as the buttons: inert while another overlay owns input, while loading, and in the
|
||||||
|
/// no-player state. Returns true when a hotkey acted this frame.</summary>
|
||||||
|
private bool HandleHotkeys(bool browserOpen, bool resultsOpen, bool poseOpen)
|
||||||
|
{
|
||||||
|
if (browserOpen || resultsOpen) return false;
|
||||||
|
if (MatchManager.Instance && MatchManager.Instance.IsLoadingLocally) return false;
|
||||||
|
if (!gameMode || gameMode.EffectiveMode == null) return false;
|
||||||
|
|
||||||
|
#if ENABLE_INPUT_SYSTEM
|
||||||
|
bool posePressed = InputUtil.GameplayKeyPressed(Key.C);
|
||||||
|
bool paintPressed = InputUtil.GameplayKeyPressed(Key.V);
|
||||||
|
#else
|
||||||
|
bool posePressed = InputUtil.GameplayKeyPressed(KeyCode.C);
|
||||||
|
bool paintPressed = InputUtil.GameplayKeyPressed(KeyCode.V);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (posePressed)
|
||||||
|
{
|
||||||
|
if (poseOpen) poseMenu.Close();
|
||||||
|
else if (poseMenu && poseMenu.CanOpen) OnPose();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (paintPressed && !poseOpen)
|
||||||
|
{
|
||||||
|
SwitchMode<PaintMode>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Esc is NOT TypingGuard-gated: it must work even while a text field has focus.
|
// Esc is NOT TypingGuard-gated: it must work even while a text field has focus.
|
||||||
// Tab (alternative binding) IS gated so it stays usable for typing/field navigation.
|
// Tab (alternative binding) IS gated so it stays usable for typing/field navigation.
|
||||||
private static bool EscapePressed()
|
private static bool EscapePressed()
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using TMPro;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.UI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The pose selection menu (the "Pose UI" canvas in Main): a modal pause overlay (same pattern as
|
||||||
|
/// <see cref="MatchResultScreen"/>) opened via the overview menu's Pose button, closed by Esc through
|
||||||
|
/// <see cref="OverviewMenu"/> (all Esc handling lives there). On every open it rebuilds a radial menu
|
||||||
|
/// of Icon Button prefab instances from the local player's <see cref="PlayerPoseController.Poses"/>
|
||||||
|
/// (icon + title come from each <see cref="PlayerPose"/>); clicking an item applies that pose and
|
||||||
|
/// closes the menu. The active pose's button is disabled (its hover ring marks it, like the overview
|
||||||
|
/// menu's mode buttons). Only the visual <see cref="panelRoot"/> is toggled - this binder stays alive
|
||||||
|
/// on its (always-active) canvas root.
|
||||||
|
/// </summary>
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
public class PoseMenu : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Tooltip("Mode manager supplying the local player (whose PlayerPoseController is driven) and the " +
|
||||||
|
"pause hook. Empty = found automatically.")]
|
||||||
|
[SerializeField] private GameModeManager gameMode;
|
||||||
|
|
||||||
|
[Header("Overlay")]
|
||||||
|
[Tooltip("Visual container toggled via Open/Close. Must be a CHILD of this binder's object so the " +
|
||||||
|
"binder stays alive while the panel is hidden.")]
|
||||||
|
[SerializeField] private GameObject panelRoot;
|
||||||
|
|
||||||
|
[Header("Radial menu")]
|
||||||
|
[Tooltip("Center the pose items are arranged around (typically a centered child of the panel).")]
|
||||||
|
[SerializeField] private RectTransform itemContainer;
|
||||||
|
|
||||||
|
[Tooltip("Icon Button prefab instantiated per pose (Icon child image + Label child TMP are bound).")]
|
||||||
|
[SerializeField] private GameObject itemPrefab;
|
||||||
|
|
||||||
|
[Tooltip("Distance of each item from the container center, in canvas units.")]
|
||||||
|
[Min(0f)]
|
||||||
|
[SerializeField] private float radius = 220f;
|
||||||
|
|
||||||
|
private readonly List<GameObject> _items = new();
|
||||||
|
private PlayerPoseController _poseController; // controller the open menu is bound to
|
||||||
|
private bool _open;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
if (!gameMode) gameMode = FindFirstObjectByType<GameModeManager>();
|
||||||
|
if (panelRoot) panelRoot.SetActive(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>True while the modal overlay is open. The overview menu yields Esc to this overlay
|
||||||
|
/// (close-only) while it is open.</summary>
|
||||||
|
public bool IsOpen => _open;
|
||||||
|
|
||||||
|
/// <summary>True while the local player has selectable poses - the overview menu hides its Pose
|
||||||
|
/// button otherwise (no player bound, or an empty pose list).</summary>
|
||||||
|
public bool CanOpen => LocalPoseController() && LocalPoseController().Poses.Count > 0;
|
||||||
|
|
||||||
|
/// <summary>Opens the modal overlay and (re)builds the radial items; false when there is nothing
|
||||||
|
/// to show, so the caller can drop the pause instead of stranding it.</summary>
|
||||||
|
public bool Open()
|
||||||
|
{
|
||||||
|
PlayerPoseController pose = LocalPoseController();
|
||||||
|
if (!pose || pose.Poses.Count == 0 || !itemContainer || !itemPrefab) return false;
|
||||||
|
_poseController = pose;
|
||||||
|
BuildItems(pose);
|
||||||
|
_open = true;
|
||||||
|
if (panelRoot) panelRoot.SetActive(true);
|
||||||
|
if (gameMode) gameMode.SetPaused(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Closes the modal overlay and resumes gameplay (Esc handling in <see cref="OverviewMenu"/>).</summary>
|
||||||
|
public void Close()
|
||||||
|
{
|
||||||
|
_open = false;
|
||||||
|
if (panelRoot) panelRoot.SetActive(false);
|
||||||
|
if (gameMode) gameMode.SetPaused(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
// The bound player went away underneath the open menu (despawn/disconnect) -> don't linger
|
||||||
|
// as a stale pause overlay.
|
||||||
|
if (_open && !_poseController)
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayerPoseController LocalPoseController()
|
||||||
|
{
|
||||||
|
PlayerCharacterController player = gameMode ? gameMode.Player : null;
|
||||||
|
return player ? player.GetComponent<PlayerPoseController>() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Instantiates one Icon Button per pose, evenly on a circle starting at 12 o'clock going
|
||||||
|
/// clockwise. Rebuilt on every open - pose count is tiny and the list can differ per bound player.</summary>
|
||||||
|
private void BuildItems(PlayerPoseController pose)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _items.Count; i++)
|
||||||
|
if (_items[i]) Destroy(_items[i]);
|
||||||
|
_items.Clear();
|
||||||
|
|
||||||
|
IReadOnlyList<PlayerPose> poses = pose.Poses;
|
||||||
|
for (int i = 0; i < poses.Count; i++)
|
||||||
|
{
|
||||||
|
GameObject item = Instantiate(itemPrefab, itemContainer);
|
||||||
|
float angle = (90f - i * 360f / poses.Count) * Mathf.Deg2Rad;
|
||||||
|
((RectTransform)item.transform).anchoredPosition =
|
||||||
|
new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * radius;
|
||||||
|
|
||||||
|
PlayerPose p = poses[i];
|
||||||
|
Transform iconChild = item.transform.Find("Icon");
|
||||||
|
Image icon = iconChild ? iconChild.GetComponent<Image>() : null;
|
||||||
|
if (icon)
|
||||||
|
{
|
||||||
|
icon.sprite = p ? p.Icon : null;
|
||||||
|
icon.preserveAspect = true;
|
||||||
|
icon.enabled = icon.sprite; // no icon authored -> no solid white square
|
||||||
|
}
|
||||||
|
TMP_Text label = item.GetComponentInChildren<TMP_Text>(true);
|
||||||
|
if (label) label.text = p && !string.IsNullOrEmpty(p.Title) ? p.Title : (p ? p.name : "");
|
||||||
|
|
||||||
|
Button button = item.GetComponent<Button>();
|
||||||
|
if (button)
|
||||||
|
{
|
||||||
|
int index = i;
|
||||||
|
button.onClick.AddListener(() => OnItem(index));
|
||||||
|
// Disabled = current pose; its hover ring (IconButtonHover) marks it as active.
|
||||||
|
button.interactable = pose.CurrentIndex != i;
|
||||||
|
}
|
||||||
|
_items.Add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnItem(int index)
|
||||||
|
{
|
||||||
|
if (_poseController) _poseController.SetPoseIndex(index);
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b5007214fcdfdff41a4f97efacf19fbb
|
||||||
@@ -8,6 +8,13 @@ using UnityEngine;
|
|||||||
[CreateAssetMenu(fileName = "NewPose", menuName = "Chameleon/Pose")]
|
[CreateAssetMenu(fileName = "NewPose", menuName = "Chameleon/Pose")]
|
||||||
public class PlayerPose : ScriptableObject
|
public class PlayerPose : ScriptableObject
|
||||||
{
|
{
|
||||||
|
[Header("UI")]
|
||||||
|
[Tooltip("Icon shown for this pose in the pose selection menu.")]
|
||||||
|
[SerializeField] private Sprite icon;
|
||||||
|
|
||||||
|
[Tooltip("Display name shown for this pose in the pose selection menu.")]
|
||||||
|
[SerializeField] private string title;
|
||||||
|
|
||||||
[Tooltip("Clip held while the player is idle in this pose (looped if the clip itself loops, " +
|
[Tooltip("Clip held while the player is idle in this pose (looped if the clip itself loops, " +
|
||||||
"otherwise frozen on its last frame). Authored for the paintman generic rig; clips for " +
|
"otherwise frozen on its last frame). Authored for the paintman generic rig; clips for " +
|
||||||
"other hierarchies will not retarget.")]
|
"other hierarchies will not retarget.")]
|
||||||
@@ -68,6 +75,8 @@ public class PlayerPose : ScriptableObject
|
|||||||
[SerializeField] private float firstPersonCameraTargetYOffset;
|
[SerializeField] private float firstPersonCameraTargetYOffset;
|
||||||
|
|
||||||
// Read-only accessors; semantics are documented on the serialized fields' tooltips above.
|
// Read-only accessors; semantics are documented on the serialized fields' tooltips above.
|
||||||
|
public Sprite Icon => icon;
|
||||||
|
public string Title => title;
|
||||||
public AnimationClip Clip => clip;
|
public AnimationClip Clip => clip;
|
||||||
public float ClipSpeed => clipSpeed;
|
public float ClipSpeed => clipSpeed;
|
||||||
public AnimationClip WalkClip => walkClip;
|
public AnimationClip WalkClip => walkClip;
|
||||||
|
|||||||
Reference in new issue
Block a user