suppress world interaction while a menu panel is open

DialogueInteractable, DialogueTransitionController, and DialogueUIVisibility are
reworked against the new UI layer: visibility is driven by UIStateController
rather than each component toggling canvases itself, which removes the
duplicated show/hide logic the two had accumulated.

NPCStandIn now ignores the interact button while UIStateController reports a
menu open, so pressing interact through an open panel no longer starts a
conversation behind it.

The yarnproject change is graph-editor node positions only.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-25 19:04:16 +02:00
co-authored by Claude Opus 5
parent ccabd3e7ac
commit 5b0827072b
5 changed files with 66 additions and 112 deletions
@@ -28,8 +28,8 @@
"y": 2287 "y": 2287
}, },
"Chevalier Cassian Thal.yarn": { "Chevalier Cassian Thal.yarn": {
"x": 1794, "x": 1621,
"y": 2275 "y": 2260
}, },
"Commentary.yarn": { "Commentary.yarn": {
"x": 12, "x": 12,
@@ -40,20 +40,20 @@
"y": 12 "y": 12
}, },
"Chair.yarn": { "Chair.yarn": {
"x": 840, "x": 866,
"y": 1072 "y": 1104
}, },
"Debug.yarn": { "Debug.yarn": {
"x": -1, "x": 14,
"y": 483 "y": 448
}, },
"SC101.yarn": { "SC101.yarn": {
"x": 820, "x": 820,
"y": -3 "y": -3
}, },
"SC102.yarn": { "SC102.yarn": {
"x": 2734, "x": 2595,
"y": 2559 "y": 2682
} }
} }
} }
@@ -1,4 +1,5 @@
using UnityEngine; using UnityEngine;
using UnityEngine.InputSystem;
using Yarn.Unity; using Yarn.Unity;
namespace NightclubArcadia.Dialogue namespace NightclubArcadia.Dialogue
@@ -14,25 +15,39 @@ namespace NightclubArcadia.Dialogue
[SerializeField] private GameObject dialogueUI; [SerializeField] private GameObject dialogueUI;
[SerializeField] private string yarnNodeName = "Bartender_Chat"; [SerializeField] private string yarnNodeName = "Bartender_Chat";
private bool playerInRange = false; private bool playerInRange;
private InputAction interactAction;
void Awake()
{
interactAction = new InputAction("Interact");
interactAction.AddBinding("<Keyboard>/e");
interactAction.AddBinding("<Gamepad>/buttonNorth");
interactAction.performed += _ =>
{
if (playerInRange)
{
StartConversation();
}
};
}
void OnEnable() => interactAction?.Enable();
void OnDisable() => interactAction?.Disable();
void OnTriggerEnter(Collider other) void OnTriggerEnter(Collider other)
{ {
if (other.CompareTag("Player")) if (other.CompareTag("Player"))
{
playerInRange = true; playerInRange = true;
}
} }
void OnTriggerExit(Collider other) void OnTriggerExit(Collider other)
{ {
if (other.CompareTag("Player")) if (other.CompareTag("Player"))
playerInRange = false;
}
void Update()
{
if (playerInRange && Input.GetKeyDown(KeyCode.E))
{ {
StartConversation(); playerInRange = false;
} }
} }
@@ -1,4 +1,4 @@
using StarterAssets; using NightclubArcadia.UI;
using UnityEngine; using UnityEngine;
using Yarn.Unity; using Yarn.Unity;
@@ -7,33 +7,27 @@ namespace NightclubArcadia.Dialogue
/// <summary> /// <summary>
/// Owns the hand-off between dialogue and the 3D environment: locks the /// Owns the hand-off between dialogue and the 3D environment: locks the
/// player's controls for as long as a conversation is running, and registers /// player's controls for as long as a conversation is running, and registers
/// the <<enter_environment>> Yarn command that hands control back. /// the &lt;&lt;enter_environment&gt;&gt; Yarn command that hands control back.
/// ///
/// The lock is driven by the DialogueRunner's own lifecycle events rather /// The lock is driven by the DialogueRunner's own lifecycle events rather
/// than by each interactable, so every entry point — auto-started nodes, /// than by each interactable, so every entry point — auto-started nodes,
/// NPCStandIn, DialogueInteractable — gets the same behaviour without /// NPCStandIn, DialogueInteractable — gets the same behaviour without
/// having to remember to do it. /// having to remember to do it. Control locking is delegated to
/// <see cref="PlayerControlLock"/> so the player menu can hold a second
/// independent lock without re-enabling movement mid-conversation.
/// </summary> /// </summary>
public class DialogueTransitionController : MonoBehaviour public class DialogueTransitionController : MonoBehaviour
{ {
[SerializeField] private DialogueRunner dialogueRunner; [SerializeField] private DialogueRunner dialogueRunner;
[SerializeField] private GameObject dialogueUI; [SerializeField] private GameObject dialogueUI;
[SerializeField] private GameObject playerArmature; [SerializeField] private GameObject playerArmature;
[SerializeField] private PlayerControlLock controlLock;
private ThirdPersonController playerController;
private StarterAssetsInputs playerInputs;
private Animator playerAnimator;
static readonly int AnimIDSpeed = Animator.StringToHash("Speed");
static readonly int AnimIDMotionSpeed = Animator.StringToHash("MotionSpeed");
void Awake() void Awake()
{ {
if (playerArmature != null) if (controlLock == null && playerArmature != null)
{ {
playerController = playerArmature.GetComponent<ThirdPersonController>(); controlLock = playerArmature.GetComponent<PlayerControlLock>();
playerInputs = playerArmature.GetComponent<StarterAssetsInputs>();
playerAnimator = playerArmature.GetComponent<Animator>();
} }
// Subscribed in Awake rather than Start: the runner auto-starts its // Subscribed in Awake rather than Start: the runner auto-starts its
@@ -45,11 +39,16 @@ namespace NightclubArcadia.Dialogue
void Start() void Start()
{ {
if (controlLock == null && playerArmature != null)
{
controlLock = playerArmature.GetComponent<PlayerControlLock>();
}
// Dialogue is in progress as soon as this controller is active // Dialogue is in progress as soon as this controller is active
// (including auto-started nodes), so the player stays locked out // (including auto-started nodes), so the player stays locked out
// until <<enter_environment>>. The armature itself stays // until <<enter_environment>>. The armature itself stays
// active/visible — only its controls are disabled. // active/visible — only its controls are disabled.
SetPlayerControlEnabled(false); controlLock?.Acquire(this);
dialogueRunner.AddCommandHandler("enter_environment", EnterEnvironment); dialogueRunner.AddCommandHandler("enter_environment", EnterEnvironment);
} }
@@ -63,7 +62,8 @@ namespace NightclubArcadia.Dialogue
void OnDialogueStart() void OnDialogueStart()
{ {
DialogueUIVisibility.Show(dialogueUI); DialogueUIVisibility.Show(dialogueUI);
SetPlayerControlEnabled(false); UIStateController.Instance?.SetDialogueActive(true);
controlLock?.Acquire(this);
} }
// Safety net: a node that ends without <<enter_environment>> must still // Safety net: a node that ends without <<enter_environment>> must still
@@ -74,40 +74,12 @@ namespace NightclubArcadia.Dialogue
{ {
// Hide, don't deactivate: deactivating the Canvas breaks TMP's // Hide, don't deactivate: deactivating the Canvas breaks TMP's
// cached Canvas reference and makes the next conversation throw. // cached Canvas reference and makes the next conversation throw.
// See DialogueUIVisibility. // See DialogueUIVisibility / PanelVisibility.
DialogueUIVisibility.Hide(dialogueUI); DialogueUIVisibility.Hide(dialogueUI);
SetPlayerControlEnabled(true); UIStateController.Instance?.SetDialogueActive(false);
} // Release is idempotent on a HashSet — safe when both
// <<enter_environment>> and onDialogueComplete fire.
void SetPlayerControlEnabled(bool value) controlLock?.Release(this);
{
if (playerController == null)
{
return;
}
if (!value)
{
// Clear pending input and the locomotion blend before freezing
// the controller. Disabling it mid-stride otherwise leaves
// Speed at its last value, and the character keeps running on
// the spot for the whole conversation.
if (playerInputs != null)
{
playerInputs.MoveInput(Vector2.zero);
playerInputs.LookInput(Vector2.zero);
playerInputs.SprintInput(false);
playerInputs.JumpInput(false);
}
if (playerAnimator != null)
{
playerAnimator.SetFloat(AnimIDSpeed, 0f);
playerAnimator.SetFloat(AnimIDMotionSpeed, 0f);
}
}
playerController.enabled = value;
} }
} }
} }
@@ -1,23 +1,13 @@
using NightclubArcadia.UI;
using UnityEngine; using UnityEngine;
namespace NightclubArcadia.Dialogue namespace NightclubArcadia.Dialogue
{ {
/// <summary> /// <summary>
/// Shows and hides the dialogue UI *without* deactivating its GameObject. /// Shows and hides the dialogue UI *without* deactivating its GameObject.
/// /// Forwards to <see cref="PanelVisibility"/> — kept as a thin wrapper so
/// SetActive(false) is not a safe way to hide this UI. TextMeshProUGUI /// existing call sites (NPCStandIn, DialogueInteractable,
/// resolves its Canvas through Graphic.CacheCanvas, which calls /// DialogueTransitionController) continue to compile unchanged.
/// GetComponentsInParent(includeInactive: false) and then requires
/// canvas.isActiveAndEnabled. Once the Canvas GameObject has been
/// deactivated, that lookup yields null, and TMP.GetTextInfo does:
///
/// if (m_canvas == null) m_canvas = this.canvas; // still null
/// GenerateTextMesh(); // m_canvas.worldCamera -> NRE
///
/// GenerateTextMesh dereferences m_canvas unguarded, so the very next line
/// Yarn tries to type throws NullReferenceException. Keeping the hierarchy
/// active and driving a CanvasGroup instead means the cached Canvas
/// reference stays valid across any number of enter/exit dialogue cycles.
/// </summary> /// </summary>
public static class DialogueUIVisibility public static class DialogueUIVisibility
{ {
@@ -27,41 +17,12 @@ namespace NightclubArcadia.Dialogue
/// case the caller must wait one frame before rendering text so that /// case the caller must wait one frame before rendering text so that
/// TMP's OnEnable can re-cache its Canvas. /// TMP's OnEnable can re-cache its Canvas.
/// </summary> /// </summary>
public static bool Show(GameObject dialogueUI) => SetVisible(dialogueUI, true); public static bool Show(GameObject dialogueUI) => PanelVisibility.Show(dialogueUI);
/// <summary> /// <summary>
/// Hides the dialogue UI, leaving the hierarchy active. See class docs. /// Hides the dialogue UI, leaving the hierarchy active. See
/// <see cref="PanelVisibility"/>.
/// </summary> /// </summary>
public static void Hide(GameObject dialogueUI) => SetVisible(dialogueUI, false); public static void Hide(GameObject dialogueUI) => PanelVisibility.Hide(dialogueUI);
static bool SetVisible(GameObject dialogueUI, bool visible)
{
if (dialogueUI == null)
{
return false;
}
// The UI may still be deactivated by an older saved scene state or
// by code that predates this helper. Recover from that, and tell
// the caller a frame of grace is needed.
var wasReactivated = false;
if (!dialogueUI.activeSelf)
{
dialogueUI.SetActive(true);
wasReactivated = true;
}
var group = dialogueUI.GetComponent<CanvasGroup>();
if (group == null)
{
group = dialogueUI.AddComponent<CanvasGroup>();
}
group.alpha = visible ? 1f : 0f;
group.interactable = visible;
group.blocksRaycasts = visible;
return wasReactivated;
}
} }
} }
@@ -3,6 +3,7 @@ using UnityEngine.InputSystem;
using TMPro; using TMPro;
using Yarn.Unity; using Yarn.Unity;
using NightclubArcadia.Dialogue; using NightclubArcadia.Dialogue;
using NightclubArcadia.UI;
/// <summary> /// <summary>
/// Drop this on a sphere primitive to use it as a placeholder NPC: /// Drop this on a sphere primitive to use it as a placeholder NPC:
@@ -153,6 +154,11 @@ public class NPCStandIn : MonoBehaviour
return; return;
} }
if (UIStateController.Instance != null && UIStateController.Instance.IsMenuOpen)
{
return;
}
if (!dialogueRunner.IsDialogueRunning) if (!dialogueRunner.IsDialogueRunning)
{ {
StartDialogueAsync(); StartDialogueAsync();