Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,44 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [4.1.0] - 2026-07-02

### Added

- **Input action buttons (**`InputActionButtonBase`**)**:
- `deferExecution` — (default: `true`) defers click handling to the next frame so input does not carry over to newly activated views.
- `requireFreshPress` — (default: `true`) ignores held inputs when the component becomes active, preventing input bleeding from the previous view.
- `ValidateFreshPress` helper integrated into `InputActionButton` and `InputActionButtonHold`.
- **Prompted text field (**`PromptedTextField`**)**:
- Optional `captionText` with caption/value visibility toggling based on whether a value is set.
- Same-frame re-open guard after a prompt is dismissed.

### Changed

- **PromptedTextField** — `emptyDisplayText` and the stored value are now runtime-managed; empty display text is seeded from `captionText` on awake.

## [4.0.1] - 2026-06-28

### Fixed

- **ViewDismissalInputHandler** — `ICloseableView` resolution now uses `TryGetComponent` with cleaner parent/children fallback when closing a view.

## [4.0.0] - 2026-06-27

### 🚀 Highlights

- **Decoupled Address-Based View Routing (`ViewAddress` & `ViewLink`)**:
- **Decoupled Address-Based View Routing (**`ViewAddress` **&** `ViewLink`**)**:
- **Never drag scene references between scripts again!** Define lightweight `ViewAddress` ScriptableObject keys to represent your UI screens (e.g. `SettingsAddress`, `InventoryAddress`, `PauseMenuAddress`).
- Trigger `SentinalViewRouter.OpenView(address)` from anywhere in your codebase — if the panel is already in the scene, Sentinal brings it to focus; if it's missing, Sentinal dynamically instantiates its fallback prefab on the fly!
- Attach a **`ViewLink`** component to any standard UGUI Button to make it open an address on click automatically with zero code.
- **Dynamic Multiplayer Role Mapping (`SentinalPlayer`)**:
- Attach a `ViewLink` component to any standard UGUI Button to make it open an address on click automatically with zero code.
- **Dynamic Multiplayer Role Mapping (**`SentinalPlayer`**)**:
- Built for local multiplayer, split-screen, and couch co-op! Centralize controller assignments via `SentinalPlayer.SetPrimaryPlayer()` or assign specific role keys (`Player 1`, `Player 2`, etc.).
- Action map gates, tab controls, and back-button cancel handlers automatically adapt live when players join, leave, or rebind gamepads.
- **Managerless Stack Architecture & Fast Play Mode**:
- Replaced the scene-bound `SentinalManager` singleton with static `SentinalViewRouter`.
- Views register dynamically on enable/disable, **removing the mandatory manager GameObject from your scenes.**
- Fully supports Unity's **Fast Play Mode** (Domain Reload Disabled). Unity 2021.3 LTS through Unity 6.5 ready.
- **Strongly Typed View Grouping (`ViewGroupMask`)**:
- **Strongly Typed View Grouping (**`ViewGroupMask`**)**:
- Multi-layered UI control! Isolate gameplay HUD overlays, party feeds, and full-screen menus into distinct group channels using bitmasks (`ViewGroupMask`).
- Integrated `ViewGroupMask` into `ViewDismissalInputHandler` so cancel/back actions can be filtered to only dismiss matching UI groups.
- Features a one-click Editor generator for `SentinalViewGroups.asset` in `Assets/Resources`.
Expand Down Expand Up @@ -48,7 +70,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added `ViewAddressRegistry` for runtime registration and resolution.
- Added `ViewLink` component for zero-code UGUI button address binding.
- Added `SentinalViewRouter.OpenView(ViewAddress)` API.
- **Player role mapping utility (`SentinalPlayer`)**:
- **Player role mapping utility (**`SentinalPlayer`**)**:
- Added `SentinalPlayer` static registry API (`SetPlayer`, `GetPlayer`, `TryGetPlayer`, `SetPrimaryPlayer`).
- Added event-driven `OnPlayerChanged` callbacks to `ViewInputSystemHandler`, `ActionMapGate`, and `ViewDismissalInputHandler` for real-time input re-binding.
- Added fallback resolution to primary player (`key 0`) or `PlayerInput.all[0]`.
Expand Down
7 changes: 6 additions & 1 deletion Editor/InputSystem/InputActionSelectorDrawer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ public override void OnGUI(Rect position, SerializedProperty property, GUIConten
const float gap = 2f;
float toggleWidth = (buttonWidth * 2f) + gap;

Rect fieldRect = new Rect(contentRect.x, contentRect.y, contentRect.width - toggleWidth - 4f, contentRect.height);
Rect fieldRect = new Rect(
contentRect.x,
contentRect.y,
contentRect.width - toggleWidth - 4f,
contentRect.height
);
Rect nameBtnRect = new Rect(fieldRect.xMax + 4f, contentRect.y, buttonWidth, contentRect.height);
Rect refBtnRect = new Rect(nameBtnRect.xMax + gap, contentRect.y, buttonWidth, contentRect.height);

Expand Down
3 changes: 2 additions & 1 deletion Editor/InputSystem/ViewInputActionHandlerEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ public override void OnInspectorGUI()
var player = handler.GetPlayerInput();
if (player != null)
{
statusText += $" | PlayerIndex: {player.playerIndex} | Map: {player.currentActionMap?.name ?? "NONE"}";
statusText +=
$" | PlayerIndex: {player.playerIndex} | Map: {player.currentActionMap?.name ?? "NONE"}";
if (!shouldSubscribe)
color = EditorColors.Caution;
}
Expand Down
3 changes: 2 additions & 1 deletion Editor/InputSystem/ViewInputSystemComponentEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ public override void OnInspectorGUI()

if (player != null)
{
statusText += $" | PlayerIndex: {player.playerIndex} | Map: {player.currentActionMap?.name ?? "NONE"}";
statusText +=
$" | PlayerIndex: {player.playerIndex} | Map: {player.currentActionMap?.name ?? "NONE"}";
}
else
{
Expand Down
26 changes: 22 additions & 4 deletions Runtime/Core/ViewLink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,31 @@ public class ViewLink : MonoBehaviour
[Tooltip("The ViewAddress to navigate to when clicked.")]
private ViewAddress targetAddress;

private Button button;

private void Awake()
{
GetComponent<Button>().onClick.AddListener(() =>
button = GetComponent<Button>();
}

private void OnEnable()
{
button.onClick.AddListener(OnClick);
}

private void OnClick()
{
if (targetAddress != null)
{
if (targetAddress != null)
ViewAddressRegistry.Resolve(targetAddress)?.Open();
});
var view = ViewAddressRegistry.Resolve(targetAddress);
if (view != null)
view.Open();
}
}

private void OnDisable()
{
button.onClick.RemoveListener(OnClick);
}
}
}
59 changes: 59 additions & 0 deletions Runtime/Input/InputSystem/Components/Base/InputActionButtonBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
using UnityEngine.UI;
using System.Collections;

namespace Sentinal.InputSystem.Components
{
Expand All @@ -24,9 +25,22 @@ public abstract class InputActionButtonBase : ViewInputActionHandler
[Tooltip("This enables button visual feedback and re-selection.")]
protected bool sendPointerEvents = true;

[SerializeField]
[Tooltip(
"Defers click execution to the next frame to prevent input events carrying over to newly activated views."
)]
protected bool deferExecution = true;

[SerializeField]
[Tooltip(
"Requires the input action to be released before accepting new presses when this component becomes active, preventing input bleeding from previous views."
)]
protected bool requireFreshPress = true;

protected Button button;
protected InputAction inputAction;
protected EventSystem eventSystem;
protected bool wasPressedOnSubscribe;

protected override void Awake()
{
Expand Down Expand Up @@ -61,6 +75,8 @@ protected override void Subscribe()
return;
}

wasPressedOnSubscribe = requireFreshPress && inputAction.IsPressed();

SubscribeToInputAction();
isSubscribed = true;
}
Expand All @@ -73,9 +89,28 @@ protected override void Unsubscribe()
inputAction = null;
}

wasPressedOnSubscribe = false;
isSubscribed = false;
}

/// <summary>
/// Validates whether the current input action event is valid based on fresh press rules.
/// Returns true if the event should be processed, or false if it should be ignored.
/// </summary>
protected bool ValidateFreshPress(bool isCancelEvent = false)
{
if (!requireFreshPress || !wasPressedOnSubscribe)
return true;

if (inputAction == null)
return false;

if (!inputAction.IsPressed() || isCancelEvent)
wasPressedOnSubscribe = false;

return false;
}

/// <summary>
/// Called when subscribing to the input action. Override to subscribe to specific action events.
/// </summary>
Expand All @@ -94,6 +129,30 @@ protected void Click()
if (button == null)
return;

if (deferExecution)
{
StartCoroutine(ClickNextFrame());
}
else
{
ExecuteClick();
}
}

private IEnumerator ClickNextFrame()
{
yield return null;
ExecuteClick();
}

/// <summary>
/// Executes the actual click event logic.
/// </summary>
protected void ExecuteClick()
{
if (button == null || !button.gameObject.activeInHierarchy)
return;

if (sendPointerEvents && eventSystem != null)
{
ExecuteEvents.Execute(
Expand Down
6 changes: 6 additions & 0 deletions Runtime/Input/InputSystem/Components/InputActionButton.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ protected override void UnsubscribeFromInputAction()

private void OnActionPerformed(InputAction.CallbackContext context)
{
if (!ValidateFreshPress(isCancelEvent: false))
return;

if (sendPointerEvents)
{
if (eventSystem == null)
Expand All @@ -49,6 +52,9 @@ private void OnActionPerformed(InputAction.CallbackContext context)

private void OnActionCanceled(InputAction.CallbackContext context)
{
if (!ValidateFreshPress(isCancelEvent: true))
return;

if (sendPointerEvents)
{
if (eventSystem == null)
Expand Down
9 changes: 9 additions & 0 deletions Runtime/Input/InputSystem/Components/InputActionButtonHold.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ protected override void UnsubscribeFromInputAction()

private void OnActionStarted(InputAction.CallbackContext context)
{
if (!ValidateFreshPress(isCancelEvent: false))
return;

if (context.interaction is HoldInteraction hold)
{
SendPointerDown();
Expand All @@ -64,12 +67,18 @@ private void OnActionStarted(InputAction.CallbackContext context)

private void OnActionPerformed(InputAction.CallbackContext context)
{
if (!ValidateFreshPress(isCancelEvent: false))
return;

if (context.interaction is HoldInteraction)
Click();
}

private void OnActionCanceled(InputAction.CallbackContext context)
{
if (!ValidateFreshPress(isCancelEvent: true))
return;

if (context.interaction is HoldInteraction)
{
SendPointerUp();
Expand Down
7 changes: 2 additions & 5 deletions Runtime/Input/InputSystem/ViewDismissalInputHandler.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#if ENABLE_INPUT_SYSTEM
using System.Collections;
using Sentinal;
using UnityEngine;
using UnityEngine.InputSystem;

Expand Down Expand Up @@ -219,11 +218,9 @@ private IEnumerator CloseCurrentViewNextFrame()

if (pendingCloseView.IsActive)
{
var closeable = pendingCloseView.GetComponent<ICloseableView>();
if (closeable == null)
if (!pendingCloseView.TryGetComponent(out ICloseableView closeable))
closeable = pendingCloseView.GetComponentInParent<ICloseableView>();
if (closeable == null)
closeable = pendingCloseView.GetComponentInChildren<ICloseableView>();
closeable ??= pendingCloseView.GetComponentInChildren<ICloseableView>();

if (closeable != null)
closeable.Close();
Expand Down
31 changes: 21 additions & 10 deletions Runtime/Input/TextInput/PromptedTextField.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ namespace Sentinal.Input
public class PromptedTextField : MonoBehaviour
{
[Header("Components")]
[SerializeField]
private TextMeshProUGUI captionText;

[SerializeField]
private TextMeshProUGUI valueText;

Expand All @@ -30,19 +33,14 @@ public class PromptedTextField : MonoBehaviour
[SerializeField]
private int maxLength = 128;

[Header("Display")]
[SerializeField]
[Tooltip("Shown when the value is empty. Also stored when the user clears the field.")]
private string emptyDisplayText = string.Empty;

[SerializeField]
private string value = string.Empty;

[Header("Events")]
public UnityEvent<string> OnValueChanged;

private Button button;
private bool isPromptOpen;
private string emptyDisplayText = string.Empty;
private string value = string.Empty;
private int promptClosedFrame = -1;

public bool IsPromptOpen => isPromptOpen;

Expand Down Expand Up @@ -70,6 +68,7 @@ private void Awake()
valueText = label.GetComponent<TextMeshProUGUI>();
}

emptyDisplayText = captionText != null ? captionText.text : string.Empty;
RefreshLabel();
}

Expand Down Expand Up @@ -106,7 +105,7 @@ public string GetEffectiveValue()

public void OpenPrompt()
{
if (isPromptOpen || !isActiveAndEnabled)
if (isPromptOpen || !isActiveAndEnabled || Time.frameCount == promptClosedFrame)
return;

if (!TextInputGateway.IsAvailable)
Expand All @@ -127,10 +126,15 @@ public void OpenPrompt()
prompt,
confirmed =>
{
promptClosedFrame = Time.frameCount;
isPromptOpen = false;
ApplyConfirmedValue(confirmed);
},
() => isPromptOpen = false
() =>
{
promptClosedFrame = Time.frameCount;
isPromptOpen = false;
}
);
}

Expand Down Expand Up @@ -164,6 +168,13 @@ private void RefreshLabel()
return;

valueText.SetText(GetDisplayText(value));
bool hasValue = !string.IsNullOrEmpty(GetEffectiveValue());

if (valueText.gameObject.activeSelf != hasValue)
valueText.gameObject.SetActive(hasValue);

if (captionText != null && captionText.gameObject.activeSelf == hasValue)
captionText.gameObject.SetActive(!hasValue);
}

private string GetPromptInitialValue(string currentValue)
Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
{
"name": "com.tirt.sentinal",
"version": "4.0.0",
"version": "4.1.0",
"displayName": "Sentinal",
"description": "A package that provides a set of tools to manage menu/view navigation and auto-selection in Unity.",
"unity": "2021.3",
"unityRelease": "0f1",
"documentationUrl": "README.md",
"changelogUrl": "CHANGELOG.md",
"licensesUrl": "LICENSE.md",
"keywords": [
"sentinal",
Expand Down