/* Yarn Spinner is licensed to you under the terms found in the file LICENSE.md. */ using System.Collections.Generic; using System.Threading; using UnityEngine; using Yarn.Markup; using Yarn.Unity.Attributes; #if USE_TMP using TMPro; #else using TextMeshProUGUI = Yarn.Unity.TMPShim; using TMP_Text = Yarn.Unity.TMPShim; #endif #nullable enable namespace Yarn.Unity { public static class InputSystemAvailability { #if USE_INPUTSYSTEM internal const bool inputSystemInstalled = true; #else internal const bool inputSystemInstalled = false; #endif #if ENABLE_INPUT_SYSTEM internal const bool enableInputSystem = true; #else internal const bool enableInputSystem = false; #endif #if ENABLE_LEGACY_INPUT_MANAGER internal const bool enableLegacyInput = true; #else internal const bool enableLegacyInput = false; #endif #if !ENABLE_LEGACY_INPUT_MANAGER /// /// A dictionary mapping legacy keycodes to Input System keys. /// static System.Lazy> lookup = new(() => { var result = new Dictionary(); foreach (KeyCode keyCode in System.Enum.GetValues(typeof(KeyCode))) { // Attempt to automatically find the equivalent of keyCode by // assuming that the string representation of a key (e.g. "Tab") // is the same in both enums. if (System.Enum.TryParse(keyCode.ToString(), true, out var value)) { result[keyCode] = value; } } // Manually map some remaining keys result[KeyCode.Return] = UnityEngine.InputSystem.Key.Enter; result[KeyCode.KeypadEnter] = UnityEngine.InputSystem.Key.NumpadEnter; return result; }); #endif /// /// Gets a value indicating whether the key indicated by a was pressed this frame. /// /// /// If the Legacy Input Manager is enabled, this method wraps . Otherwise, it attempts to find the equivalent of , and then checks with to find the key, /// and queries its /// property. /// /// The to check for the state /// of. /// Whether the key was pressed this frame. public static bool GetKeyDown(KeyCode key) { if (key == KeyCode.None) { // The 'none' key is never pressed return false; } #if ENABLE_LEGACY_INPUT_MANAGER // If we're using Legacy Input, read from it directly return Input.GetKeyDown(key); #else if (lookup.Value.TryGetValue(key, out var lookupKey)) { try { return UnityEngine.InputSystem.Keyboard.current[lookup.Value[key]].wasPressedThisFrame; } catch (System.ArgumentOutOfRangeException) { #if DEBUG Debug.LogWarning($"Can't check if {key} is down: found Input System mapping {lookupKey}, but this key is not present in the current keyboard"); #endif return false; } } else { #if DEBUG Debug.LogWarning($"Can't check if {key} is down: can't find a mapping from legacy keycode {key} to Unity Input System"); #endif return false; } #endif } public static bool GetButtonDown(string? buttonName) { if (buttonName == null) { return false; } #if ENABLE_LEGACY_INPUT_MANAGER return Input.GetButtonUp(buttonName); #else return false; #endif } public static float GetAxis(string? axisName) { if (axisName == null) { return 0; } #if ENABLE_LEGACY_INPUT_MANAGER return Input.GetAxis(axisName); #else return 0; #endif } } /// /// A dialogue presenter that listens for user input and sends requests to a to advance the presentation of the current line, /// either by asking a dialogue runner to hurry up its delivery, advance to /// the next line, or cancel the entire dialogue session. /// public class LineAdvancer : DialoguePresenterBase, IActionMarkupHandler { [MustNotBeNull("Line Advancer needs to know which Dialogue Runner should be told to tell it to show the next line.")] [Tooltip("The dialogue runner that will receive requests to advance or cancel content.")] [SerializeField] protected DialogueRunner? runner; /// /// The that this LineAdvancer should subscribe to for notifications that the line is fully visible. /// /// When is called, if the line is fully visible, the object will have its method called (instead of its method). /// This behaviour is only the case when the is set to false. /// [SerializeField] protected DialoguePresenterBase? presenter; /// /// Should this line advancer use different actions for hurrying up a line and advancing a line? /// /// /// When this is false if the player requests a line to hurry up and the line is fully shown the method will be called instead of the method. /// This behaviour is only the case when is not null and the presenter is presenting it's line content via it's property. /// [SerializeField] protected bool separateHurryUpAndAdvanceControls = false; /// /// If , repeatedly signalling that the line /// should be hurried up will cause the line advancer to request that /// the next line be shown. /// /// [Space] [Tooltip("Does repeatedly requesting a line advance cancel the line?")] public bool multiAdvanceIsCancel = false; /// /// The number of times that a 'hurry up' signal occurs before the line /// advancer requests that the next line be shown. /// /// [ShowIf(nameof(multiAdvanceIsCancel))] [Indent] [Label("Advance Count")] [Tooltip("The number of times that a line advance occurs before the current line is cancelled.")] public int advanceRequestsBeforeCancellingLine = 2; /// /// The number of times that this object has received an indication that /// the line should be advanced. /// /// /// This value is reset to zero when a new line is run. When the line is /// advanced, this value is incremented. If this value ever meets or /// exceeds , the line /// will be cancelled. /// private int numberOfAdvancesThisLine = 0; /// /// The type of input that this line advancer responds to. /// public enum InputMode { /// /// The line advancer responds to Input Actions from the Unity /// Input System. /// InputActions, /// /// The line advancer responds to keypresses on the keyboard. /// KeyCodes, /// /// The line advancer does not respond to any input. /// /// When a line advancer's is set /// to , call the , and /// methods directly from /// your code to control line advancement. None, /// /// The line advancer responds to input from the legacy Input /// Manager. /// LegacyInputAxes, } /// /// The type of input that this line advancer responds to. /// /// [Tooltip("The type of input that this line advancer responds to.")] [Space] [MessageBox(sourceMethod: nameof(ValidateInputMode))] [SerializeField] protected InputMode inputMode; // when using the same input for different actions, for example using spacebar to select an option but also spacebar to hurry up lines // the action for hurrying up the line will happen the same frame as the action for selection // so if a line follows options (very common), that line might well get told to instantly hurry up // which isn't ideal, so this tracks the frame that content arrives and hurry up events cannot run the same frame as their content appears private int frameContentReceived = 0; protected InputMode UsedInputMode { get { const bool inputSystemAvailable = InputSystemAvailability.enableInputSystem && InputSystemAvailability.inputSystemInstalled; if (inputMode == InputMode.InputActions && !inputSystemAvailable) { // We're configured to use input actions, but the input // system is not enabled. Fall back to key codes. return InputMode.KeyCodes; } else { return inputMode; } } } /// /// Validates the current value of , and /// potentially returns a message box to display. /// protected MessageBoxAttribute.Message ValidateInputMode() { #pragma warning disable CS0162 // Unreachable code detected if (this.inputMode == InputMode.None) { return MessageBoxAttribute.Info($"To use this component, call the following methods on it:\n\n" + $"- {nameof(this.RequestLineHurryUp)}()\n" + $"- {nameof(this.RequestNextLine)}()\n" + $"- {nameof(this.RequestOptionHurryUp)}()\n" + $"- {nameof(this.RequestDialogueCancellation)}()" ); } if (this.inputMode == InputMode.LegacyInputAxes && !InputSystemAvailability.enableLegacyInput) { return MessageBoxAttribute.Warning("The Input Manager (Old) system is not enabled.\n\nEither change this setting to Input Actions, or enable Input Manager (Old) in Project Settings > Player > Configuration > Active Input Handling."); } if (this.inputMode == InputMode.InputActions) { if (InputSystemAvailability.inputSystemInstalled == false) { return MessageBoxAttribute.Warning("Please install the Unity Input System package to use Input Actions.\n\nFalling back to the keyboard in the meantime."); } if (!InputSystemAvailability.enableInputSystem) { return MessageBoxAttribute.Warning("The Unity Input System is not enabled.\n\nEither change this setting, or enable Input System in Project Settings > Player > Configuration > Active Input Handling.\n\nFalling back to the keyboard in the meantime."); } } return MessageBoxAttribute.NoMessage; #pragma warning restore CS0162 // Unreachable code detected } #if USE_INPUTSYSTEM /// /// The Input Action that triggers a request to advance to the next /// piece of content. /// [ShowIf(nameof(UsedInputMode), InputMode.InputActions)] [Indent] [SerializeField] protected UnityEngine.InputSystem.InputActionReference? hurryUpLineAction; /// /// The Input Action that triggers an instruction to cancel the current /// line. /// [ShowIf(nameof(UsedInputMode), InputMode.InputActions)] [ShowIf(nameof(separateHurryUpAndAdvanceControls))] [Indent] [SerializeField] protected UnityEngine.InputSystem.InputActionReference? nextLineAction; /// /// The Input Action that triggers an instruction to hurry up presenting the current options /// [ShowIf(nameof(UsedInputMode), InputMode.InputActions)] [Indent] [SerializeField] protected UnityEngine.InputSystem.InputActionReference? hurryUpOptionsAction; /// /// The Input Action that triggers an instruction to cancel the entire /// dialogue. /// [ShowIf(nameof(UsedInputMode), InputMode.InputActions)] [Indent] [SerializeField] protected UnityEngine.InputSystem.InputActionReference? cancelDialogueAction; /// /// If true, the , and Input /// Actions will be enabled when the the dialogue runner signals that a /// line is running. /// [Tooltip("If true, the input actions above will be enabled when a line begins.")] [ShowIf(nameof(UsedInputMode), InputMode.InputActions)] [Indent] [SerializeField] protected bool enableActions = true; #endif /// /// The legacy Input Axis that triggers a request to advance to the next /// piece of content. /// [ShowIf(nameof(UsedInputMode), InputMode.LegacyInputAxes)] [Indent] [SerializeField] protected string? hurryUpLineAxis = "Jump"; /// /// The legacy Input Axis that triggers an instruction to cancel the /// current line. /// [ShowIf(nameof(UsedInputMode), InputMode.LegacyInputAxes)] [ShowIf(nameof(separateHurryUpAndAdvanceControls))] [Indent] [SerializeField] protected string? nextLineAxis = "Cancel"; /// /// The legacy Input Axis that triggers an instruction to hurry up presenting the current options /// [ShowIf(nameof(UsedInputMode), InputMode.LegacyInputAxes)] [Indent] [SerializeField] protected string? hurryUpOptionsAxis = "Jump"; /// /// The legacy Input Axis that triggers an instruction to cancel the /// entire dialogue. /// [ShowIf(nameof(UsedInputMode), InputMode.LegacyInputAxes)] [Indent] [SerializeField] protected string? cancelDialogueAxis = ""; /// /// The that triggers a request to advance to the /// next piece of content. /// [ShowIf(nameof(UsedInputMode), InputMode.KeyCodes)] [Indent] [SerializeField] protected KeyCode hurryUpLineKeyCode = KeyCode.Space; /// /// The that triggers an instruction to cancel the /// current line. /// [ShowIf(nameof(UsedInputMode), InputMode.KeyCodes)] [ShowIf(nameof(separateHurryUpAndAdvanceControls))] [Indent] [SerializeField] protected KeyCode nextLineKeyCode = KeyCode.Escape; /// /// The that triggers an instruction to hurry up presenting options /// [ShowIf(nameof(UsedInputMode), InputMode.KeyCodes)] [Indent] [SerializeField] protected KeyCode hurryUpOptionsKeyCode = KeyCode.Space; /// /// The that triggers an instruction to cancel the /// entire dialogue. /// [ShowIf(nameof(UsedInputMode), InputMode.KeyCodes)] [Indent] [SerializeField] protected KeyCode cancelDialogueKeyCode = KeyCode.None; #if USE_INPUTSYSTEM private void OnHurryUpLinePerformed(UnityEngine.InputSystem.InputAction.CallbackContext ctx) { RequestLineHurryUpInternal(); } private void OnHurryUpOptionsPerformed(UnityEngine.InputSystem.InputAction.CallbackContext ctx) { RequestOptionHurryUp(); } private void OnNextLinePerformed(UnityEngine.InputSystem.InputAction.CallbackContext ctx) { RequestNextLine(); } private void OnCancelDialoguePerformed(UnityEngine.InputSystem.InputAction.CallbackContext ctx) { RequestDialogueCancellation(); } #endif // used to track the status of the presentation // you can think of this as a variation on multiple presses to advance a line // where if the presenter is awaiting input it is reasonable that pressing hurry up would advance to the next piece of content // but the default presenters can't really tell that apart // so the line advancer instead will handle this // this only works if the line advancer is added as a processor onto the presenters typewriter // but that is ok as that is the default // as people replace those defaults with more complex views and presenters they will also want to replace the line advancer anyways private enum PresentationStatus { Unknown, LineBegan, LineWaiting, OptionsBegan, OptionsWaiting, } private PresentationStatus status = PresentationStatus.Unknown; private void Start() { // If we have a dialogue presenter configured, register ourselves as // a temporal processor, so that we get notified when the line is // fully visible. This is so that when a line is fully visible, the // 'hurry up' action will instead trigger a 'next line' action, // (because there's nothing left to hurry up.) if (runner == null || presenter == null) { return; } if (!separateHurryUpAndAdvanceControls) { var listOfPresenters = new List(runner.DialoguePresenters) { this }; runner.DialoguePresenters = listOfPresenters; presenter.Typewriter?.ActionMarkupHandlers.Add(this); // last thing is to null out the inputs just in case nextLineAxis = null; nextLineKeyCode = KeyCode.None; #if USE_INPUTSYSTEM nextLineAction = null; #endif } } /// /// Called by a dialogue runner when dialogue starts to add input action /// handlers for advancing the line. /// /// A completed task. public override YarnTask OnDialogueStartedAsync() { #if USE_INPUTSYSTEM if (enableActions) { if (hurryUpLineAction != null) { hurryUpLineAction.action.Enable(); } if (hurryUpOptionsAction != null) { hurryUpOptionsAction.action.Enable(); } if (nextLineAction != null) { nextLineAction.action.Enable(); } if (cancelDialogueAction != null) { cancelDialogueAction.action.Enable(); } } if (UsedInputMode == InputMode.InputActions) { // If we're using the input system, register callbacks to run // when our actions are performed. if (hurryUpLineAction != null) { hurryUpLineAction.action.performed += OnHurryUpLinePerformed; } if (hurryUpOptionsAction != null) { hurryUpOptionsAction.action.performed += OnHurryUpOptionsPerformed; } if (nextLineAction != null) { nextLineAction.action.performed += OnNextLinePerformed; } if (cancelDialogueAction != null) { cancelDialogueAction.action.performed += OnCancelDialoguePerformed; } } #endif ResetLineTracking(); return YarnTask.CompletedTask; } /// /// Called by a dialogue runner when dialogue ends to remove the input /// action handlers. /// /// A completed task. public override YarnTask OnDialogueCompleteAsync() { #if USE_INPUTSYSTEM // If we're using the input system, remove the callbacks. if (UsedInputMode == InputMode.InputActions) { if (hurryUpLineAction != null) { hurryUpLineAction.action.performed -= OnHurryUpLinePerformed; } if (hurryUpOptionsAction != null) { hurryUpOptionsAction.action.performed -= OnHurryUpOptionsPerformed; } if (nextLineAction != null) { nextLineAction.action.performed -= OnNextLinePerformed; } if (cancelDialogueAction != null) { cancelDialogueAction.action.performed -= OnCancelDialoguePerformed; } } #endif ResetLineTracking(); return YarnTask.CompletedTask; } /// /// Called by a dialogue presenter to signal that a line is running. /// /// /// A completed task. public override YarnTask RunLineAsync(LocalizedLine line, LineCancellationToken token) { // A new line has come in, so reset the number of times we've seen a // request to skip. ResetLineTracking(); status = PresentationStatus.LineBegan; frameContentReceived = Time.frameCount; return YarnTask.CompletedTask; } /// /// Called by a dialogue presenter to signal that options are running. /// /// /// A completed task indicating that no option was selected by /// this view. public override YarnTask RunOptionsAsync(DialogueOption[] dialogueOptions, LineCancellationToken cancellationToken) { ResetLineTracking(); status = PresentationStatus.OptionsBegan; frameContentReceived = Time.frameCount; return DialogueRunner.NoOptionSelected; } private void ResetLineTracking() { numberOfAdvancesThisLine = 0; status = PresentationStatus.Unknown; } protected virtual void RequestLineHurryUpInternal() { if (frameContentReceived == Time.frameCount) { return; } // in this mode we NEED to be in a state where a line showing, regardless of it's completion state if (!separateHurryUpAndAdvanceControls) { if (!(status == PresentationStatus.LineBegan || status == PresentationStatus.LineWaiting)) { return; } } // Increment our counter of line advancements, and depending on the // new count, request that the runner 'soft-cancel' the line or // cancel the entire line // this is true regardless of if we are the hurry up mode or not numberOfAdvancesThisLine += 1; if (multiAdvanceIsCancel && numberOfAdvancesThisLine >= advanceRequestsBeforeCancellingLine) { RequestNextLine(); } else { // at this stage we want to hurry up if we are in multiAdvanceIsCancel // and either hurry up or skip the line depending on the state if (separateHurryUpAndAdvanceControls) { if (runner != null) { runner.RequestHurryUpLine(); } else { Debug.LogError($"{nameof(LineAdvancer)} dialogue runner is null", this); } } else { if (status == PresentationStatus.LineWaiting) { RequestNextLine(); } else { if (runner != null) { runner.RequestHurryUpLine(); } else { Debug.LogError($"{nameof(LineAdvancer)} dialogue runner is null", this); } } } } } /// /// Requests that the line be hurried up. /// /// If this method has been called more times for a single line /// than , this method requests /// that the dialogue runner proceed to the next line. Otherwise, it /// requests that the dialogue runner instruct all line views to hurry /// up their presentation of the current line. /// public void RequestLineHurryUp() { // Increment our counter of line advancements, and depending on the // new count, request that the runner 'soft-cancel' the line or // cancel the entire line numberOfAdvancesThisLine += 1; if (multiAdvanceIsCancel && numberOfAdvancesThisLine >= advanceRequestsBeforeCancellingLine) { RequestNextLine(); } else { if (runner != null) { runner.RequestHurryUpLine(); } else { Debug.LogError($"{nameof(LineAdvancer)} dialogue runner is null", this); } } } public void RequestOptionHurryUp() { if (frameContentReceived == Time.frameCount) { return; } if (runner == null) { Debug.LogError($"Unable to hurry up options, {nameof(LineAdvancer)} dialogue runner is null", this); return; } if (!separateHurryUpAndAdvanceControls) { if (status == PresentationStatus.OptionsBegan || status == PresentationStatus.OptionsWaiting) { runner.RequestHurryUpOption(); } } else { runner.RequestHurryUpOption(); } } /// /// Requests that the dialogue runner proceeds to the next line. /// public virtual void RequestNextLine() { ResetLineTracking(); if (runner != null) { runner.RequestNextLine(); } else { Debug.LogError($"{nameof(LineAdvancer)} dialogue runner is null", this); } } /// /// Requests that the dialogue runner to instruct all line views to /// dismiss their content, and then stops the dialogue. /// public void RequestDialogueCancellation() { ResetLineTracking(); // Stop the dialogue runner, which will cancel the current line as // well as the entire dialogue. if (runner != null) { runner.Stop().Forget(); } } /// /// Called by Unity every frame to check to see if, depending on , the should take /// action. /// private void Update() { switch (UsedInputMode) { case InputMode.KeyCodes: if (InputSystemAvailability.GetKeyDown(hurryUpLineKeyCode)) { this.RequestLineHurryUpInternal(); } if (InputSystemAvailability.GetKeyDown(hurryUpOptionsKeyCode)) { this.RequestOptionHurryUp(); } if (InputSystemAvailability.GetKeyDown(nextLineKeyCode)) { this.RequestNextLine(); } if (InputSystemAvailability.GetKeyDown(cancelDialogueKeyCode)) { this.RequestDialogueCancellation(); } break; case InputMode.LegacyInputAxes: if (InputSystemAvailability.GetButtonDown(hurryUpLineAxis)) { this.RequestLineHurryUpInternal(); } if (InputSystemAvailability.GetButtonDown(hurryUpOptionsAxis)) { this.RequestOptionHurryUp(); } if (InputSystemAvailability.GetButtonDown(nextLineAxis)) { this.RequestNextLine(); } if (InputSystemAvailability.GetButtonDown(cancelDialogueAxis)) { this.RequestDialogueCancellation(); } break; default: // Nothing to do; 'None' takes no action, and 'InputActions' // doesn't poll in Update() break; } } public void OnPrepareForLine(MarkupParseResult line, TMP_Text text) { return; } public void OnLineDisplayBegin(MarkupParseResult line, TMP_Text text) { return; } public YarnTask OnCharacterWillAppear(int currentCharacterIndex, MarkupParseResult line, CancellationToken cancellationToken) { return YarnTask.CompletedTask; } public void OnLineDisplayComplete() { if (status == PresentationStatus.LineBegan) { status = PresentationStatus.LineWaiting; } else if (status == PresentationStatus.OptionsBegan) { status = PresentationStatus.OptionsWaiting; } } public void OnLineWillDismiss() { return; } } }