// This code is part of the Fungus library (https://github.com/snozbot/fungus) // It is released for free under the MIT open source license (https://github.com/snozbot/fungus/blob/master/LICENSE) using UnityEngine; using UnityEngine.UI; using System.Collections; using UnityEngine.EventSystems; using System.Linq; using MoonSharp.Interpreter; namespace Fungus { /// <summary> /// Presents multiple choice buttons to the players. /// </summary> public class MenuDialog : MonoBehaviour { [Tooltip("Automatically select the first interactable button when the menu is shown.")] [SerializeField] protected bool autoSelectFirstButton = false; protected Button[] cachedButtons; protected Slider cachedSlider; private int nextOptionIndex; #region Public members /// <summary> /// Currently active Menu Dialog used to display Menu options /// </summary> public static MenuDialog ActiveMenuDialog { get; set; } /// <summary> /// A cached list of button objects in the menu dialog. /// </summary> /// <value>The cached buttons.</value> public virtual Button[] CachedButtons { get { return cachedButtons; } } /// <summary> /// A cached slider object used for the timer in the menu dialog. /// </summary> /// <value>The cached slider.</value> public virtual Slider CachedSlider { get { return cachedSlider; } } /// <summary> /// Sets the active state of the Menu Dialog gameobject. /// </summary> public virtual void SetActive(bool state) { gameObject.SetActive(state); } /// <summary> /// Returns a menu dialog by searching for one in the scene or creating one if none exists. /// </summary> public static MenuDialog GetMenuDialog() { if (ActiveMenuDialog == null) { // Use first Menu Dialog found in the scene (if any) var md = GameObject.FindObjectOfType<MenuDialog>(); if (md != null) { ActiveMenuDialog = md; } if (ActiveMenuDialog == null) { // Auto spawn a menu dialog object from the prefab GameObject prefab = Resources.Load<GameObject>("Prefabs/MenuDialog"); if (prefab != null) { GameObject go = Instantiate(prefab) as GameObject; go.SetActive(false); go.name = "MenuDialog"; ActiveMenuDialog = go.GetComponent<MenuDialog>(); } } } return ActiveMenuDialog; } protected virtual void Awake() { Button[] optionButtons = GetComponentsInChildren<Button>(); cachedButtons = optionButtons; Slider timeoutSlider = GetComponentInChildren<Slider>(); cachedSlider = timeoutSlider; if (Application.isPlaying) { // Don't auto disable buttons in the editor Clear(); } CheckEventSystem(); } // There must be an Event System in the scene for Say and Menu input to work. // This method will automatically instantiate one if none exists. protected virtual void CheckEventSystem() { EventSystem eventSystem = GameObject.FindObjectOfType<EventSystem>(); if (eventSystem == null) { // Auto spawn an Event System from the prefab GameObject prefab = Resources.Load<GameObject>("Prefabs/EventSystem"); if (prefab != null) { GameObject go = Instantiate(prefab) as GameObject; go.name = "EventSystem"; } } } protected virtual void OnEnable() { // The canvas may fail to update if the menu dialog is enabled in the first game frame. // To fix this we just need to force a canvas update when the object is enabled. Canvas.ForceUpdateCanvases(); } protected virtual IEnumerator WaitForTimeout(float timeoutDuration, Block targetBlock) { float elapsedTime = 0; Slider timeoutSlider = CachedSlider; while (elapsedTime < timeoutDuration) { if (timeoutSlider != null) { float t = 1f - elapsedTime / timeoutDuration; timeoutSlider.value = t; } elapsedTime += Time.deltaTime; yield return null; } Clear(); gameObject.SetActive(false); HideSayDialog(); if (targetBlock != null) { targetBlock.StartExecution(); } } protected IEnumerator CallBlock(Block block) { yield return new WaitForEndOfFrame(); block.StartExecution(); } protected IEnumerator CallLuaClosure(LuaEnvironment luaEnv, Closure callback) { yield return new WaitForEndOfFrame(); if (callback != null) { luaEnv.RunLuaFunction(callback, true); } } /// <summary> /// Clear all displayed options in the Menu Dialog. /// </summary> public virtual void Clear() { StopAllCoroutines(); //if something was shown notify that we are ending if(nextOptionIndex != 0) MenuSignals.DoMenuEnd(this); nextOptionIndex = 0; var optionButtons = CachedButtons; for (int i = 0; i < optionButtons.Length; i++) { var button = optionButtons[i]; button.onClick.RemoveAllListeners(); } for (int i = 0; i < optionButtons.Length; i++) { var button = optionButtons[i]; if (button != null) { button.transform.SetSiblingIndex(i); button.gameObject.SetActive(false); } } Slider timeoutSlider = CachedSlider; if (timeoutSlider != null) { timeoutSlider.gameObject.SetActive(false); } } /// <summary> /// Hides any currently displayed Say Dialog. /// </summary> public virtual void HideSayDialog() { var sayDialog = SayDialog.GetSayDialog(); if (sayDialog != null) { sayDialog.FadeWhenDone = true; } } /// <summary> /// Adds the option to the list of displayed options. Calls a Block when selected. /// Will cause the Menu dialog to become visible if it is not already visible. /// </summary> /// <returns><c>true</c>, if the option was added successfully.</returns> /// <param name="text">The option text to display on the button.</param> /// <param name="interactable">If false, the option is displayed but is not selectable.</param> /// <param name="hideOption">If true, the option is not displayed but the menu knows that option can or did exist</param> /// <param name="targetBlock">Block to execute when the option is selected.</param> public virtual bool AddOption(string text, bool interactable, bool hideOption, Block targetBlock) { var block = targetBlock; UnityEngine.Events.UnityAction action = delegate { EventSystem.current.SetSelectedGameObject(null); StopAllCoroutines(); // Stop timeout Clear(); HideSayDialog(); if (block != null) { var flowchart = block.GetFlowchart(); gameObject.SetActive(false); // Use a coroutine to call the block on the next frame // Have to use the Flowchart gameobject as the MenuDialog is now inactive flowchart.StartCoroutine(CallBlock(block)); } }; return AddOption(text, interactable, hideOption, action); } /// <summary> /// Adds the option to the list of displayed options, calls a Lua function when selected. /// Will cause the Menu dialog to become visible if it is not already visible. /// </summary> /// <returns><c>true</c>, if the option was added successfully.</returns> public virtual bool AddOption(string text, bool interactable, LuaEnvironment luaEnv, Closure callBack) { if (!gameObject.activeSelf) { gameObject.SetActive(true); } // Copy to local variables LuaEnvironment env = luaEnv; Closure call = callBack; UnityEngine.Events.UnityAction action = delegate { StopAllCoroutines(); // Stop timeout Clear(); HideSayDialog(); // Use a coroutine to call the callback on the next frame StartCoroutine(CallLuaClosure(env, call)); }; return AddOption(text, interactable, false, action); } /// <summary> /// Adds the option to the list of displayed options. Calls a Block when selected. /// Will cause the Menu dialog to become visible if it is not already visible. /// </summary> /// <returns><c>true</c>, if the option was added successfully.</returns> /// <param name="text">The option text to display on the button.</param> /// <param name="interactable">If false, the option is displayed but is not selectable.</param> /// <param name="hideOption">If true, the option is not displayed but the menu knows that option can or did exist</param> /// <param name="action">Action attached to the button on the menu item</param> private bool AddOption(string text, bool interactable, bool hideOption, UnityEngine.Events.UnityAction action) { if (nextOptionIndex >= CachedButtons.Length) { Debug.LogWarning("Unable to add menu item, not enough buttons: " + text); return false; } //if first option notify that a menu has started if(nextOptionIndex == 0) MenuSignals.DoMenuStart(this); var button = cachedButtons[nextOptionIndex]; //move forward for next call nextOptionIndex++; //don't need to set anything on it if (hideOption) return true; button.gameObject.SetActive(true); button.interactable = interactable; if (interactable && autoSelectFirstButton && !cachedButtons.Select(x => x.gameObject).Contains(EventSystem.current.currentSelectedGameObject)) { EventSystem.current.SetSelectedGameObject(button.gameObject); } TextAdapter textAdapter = new TextAdapter(); textAdapter.InitFromGameObject(button.gameObject, true); if (textAdapter.HasTextObject()) { text = TextVariationHandler.SelectVariations(text); textAdapter.Text = text; } button.onClick.AddListener(action); return true; } /// <summary> /// Show a timer during which the player can select an option. Calls a Block when the timer expires. /// </summary> /// <param name="duration">The duration during which the player can select an option.</param> /// <param name="targetBlock">Block to execute if the player does not select an option in time.</param> public virtual void ShowTimer(float duration, Block targetBlock) { if (cachedSlider != null) { cachedSlider.gameObject.SetActive(true); gameObject.SetActive(true); StopAllCoroutines(); StartCoroutine(WaitForTimeout(duration, targetBlock)); } else { Debug.LogWarning("Unable to show timer, no slider set"); } } /// <summary> /// Show a timer during which the player can select an option. Calls a Lua function when the timer expires. /// </summary> public virtual IEnumerator ShowTimer(float duration, LuaEnvironment luaEnv, Closure callBack) { if (CachedSlider == null || duration <= 0f) { yield break; } CachedSlider.gameObject.SetActive(true); StopAllCoroutines(); float elapsedTime = 0; Slider timeoutSlider = CachedSlider; while (elapsedTime < duration) { if (timeoutSlider != null) { float t = 1f - elapsedTime / duration; timeoutSlider.value = t; } elapsedTime += Time.deltaTime; yield return null; } Clear(); gameObject.SetActive(false); HideSayDialog(); if (callBack != null) { luaEnv.RunLuaFunction(callBack, true); } } /// <summary> /// Returns true if the Menu Dialog is currently displayed. /// </summary> public virtual bool IsActive() { return gameObject.activeInHierarchy; } /// <summary> /// Returns the number of currently displayed options. /// </summary> public virtual int DisplayedOptionsCount { get { int count = 0; for (int i = 0; i < cachedButtons.Length; i++) { var button = cachedButtons[i]; if (button.gameObject.activeSelf) { count++; } } return count; } } /// <summary> /// Shuffle the parent order of the cached buttons, allows for randomising button order, buttons are auto reordered when cleared /// </summary> public void Shuffle(System.Random r) { for (int i = 0; i < CachedButtons.Length; i++) { CachedButtons[i].transform.SetSiblingIndex(r.Next(CachedButtons.Length)); } } #endregion } }