using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using UnityEngine.Events; using UnityEngine.EventSystems; using Framework.Services; using Framework.Support; using ConditionalAttribute = System.Diagnostics.ConditionalAttribute; namespace Framework.Dialogue { /// Manages UI widgets in a canvas used to display dialogue /// /// This class is intended to help implement the IDialogueCanvas interface based /// and apply common standards to how UI elements need to be named to be /// discoverable by the dialogue canvas. /// [RequireComponent(typeof(Canvas))] public class DialogueCanvas : ScriptComponent, IDialogueCanvas { #region class ButtonEventProxy /// Forwards button events together with the assigned button index private class ButtonEventProxy : IDisposable { /// Creates a new button proxy and subscribes it to a button /// Canvas to which the button events will be delivered /// Button this proxy will forward events for /// Index of option this button represents /// A new button event proxy that has been subscribed to the button public static ButtonEventProxy CreateAndSubscribe( DialogueCanvas canvas, Button button, int buttonIndex ) { var proxy = new ButtonEventProxy(canvas, button, buttonIndex); button.onClick.AddListener(proxy.clickedDelegate); return proxy; } /// Initializes a new proxy for specified button with index /// Canvas to which button events will be sent /// Button for which the proxy will forward events /// Button index that will be reported to the canvas private ButtonEventProxy(DialogueCanvas canvas, Button button, int buttonIndex) { this.clickedDelegate = new UnityAction(clicked); this.canvas = canvas; this.buttonIndex = buttonIndex; this.button = button; } /// Unsubsribes from the buttons events public void Dispose() { if(this.button != null) { this.button.onClick.RemoveListener(this.clickedDelegate); this.button = null; } this.canvas = null; } /// Callback invoked when the user has clicked on the button private void clicked() { if(this.canvas != null) { this.canvas.Select(this.buttonIndex); } } /// Canvas to which button click events will be sent private DialogueCanvas canvas; /// Button to whose click event the proxy is subscribed private Button button; /// Button index the proxy will report to the canvas private int buttonIndex; /// Delete for the clicked() method private UnityAction clickedDelegate; } #endregion // class ButtonEventProxy /// Triggered when the user picks a choice or confirms the speech public event Action Selected; /// Displays text spoken or thought by a character /// Text that will be displayed public void ShowSpeech(string text) { ShowSpeechAndChoices(text, null); } /// Lets the player make a choice /// Choices the player can select from public void ShowChoices(string[] choices) { ShowSpeechAndChoices(null, choices); } /// /// Displays text spoken or thought by a character (or the player) and lets /// the player make a choice /// /// Text that will be displayed /// Choices the player can select from public void ShowSpeechAndChoices(string speech, string[] choices) { // Show or hide the speech panel as needed if(string.IsNullOrEmpty(speech)) { if(this.speechVisible) { TearDownSpeech(); this.speechVisible = false; } } else { SetupNewSpeech(speech); this.speechVisible = true; } int choiceCount = ArrayHelper.CountNullableArray(choices); warnIfTooManyButtons(choiceCount); // Show or hide the choice buttons as needed if(choiceCount > 0) { SetupNewChoices(choices); this.selectableChoiceCount = choiceCount; } else if(this.selectableChoiceCount > 0) { TearDownChoices(); this.selectableChoiceCount = 0; } } /// Hides the dialogue from the canvas public void Hide() { ShowSpeechAndChoices(null, null); } /// Picks a choice as if the user had clicked on a choice button /// Index of the choice that has been selected public void Select(int choiceIndex) { // If a choice was selected while the choice were not visible, // ignore this notification. Probably the user pressed a keyboard // shortcut when no choices were visible or double-selected a choice. if(choiceIndex >= this.selectableChoiceCount) { Debug.LogWarning( "Ignoring choice " + choiceIndex + " because no choice by that index " + "is currently being presented." ); return; } // If there are choices being displayed, require one of them to be selected // (the user cannot continue to next speech panel if he's asked for a choice). if(this.selectableChoiceCount > 0) { if(choiceIndex == -1) { Debug.LogWarning( "Ignoring choice -1 (speech panel) because there are choices available" ); return; } // Having picked a choice, kill the choice buttons TearDownChoices(); this.selectableChoiceCount = 0; } // The user made a valid choice or confirmed the speech panel when // it was the only widget being shown. OnSelected(choiceIndex); } /// Called once before the first update cycle of the component protected override void Awake() { base.Awake(); // This component needs to be attached to a canvas this.canvas = GetComponent(); if(this.canvas == null) { Debug.LogError( "DialogueCanvas on game object '" + gameObject.name + "' does not have " + "a 'Canvas' component. Dialogue UI will not be displayed!" ); return; } this.speechClickedDelegate = new UnityAction(speechClicked); // Look for the template controls we will use to display dialogue discoverSpeechPanel(this.canvas.gameObject); discoverButtonWidgets(this.canvas.gameObject); // Give derived classes a chance to do further intialization. // They can look for additional controls or remember original color values here InitializeWidgets(this.canvas, this.speechPanel, this.choiceButtons); // It is almost certain the UI was built the way it should look when all // controls are visible. Thus our first action should be to hide it. hideAllControlsInstantly(); } /// Called once when the component and/or game object cease to exist protected virtual void Destroy() { unsubscribeFromButtonEvents(); } /// Allows additional preparations to be made for the choice buttons /// Canvas that is hosting the controls /// Speech panel used to show dialogue text /// Choice buttons ordered from top to bottom protected virtual void InitializeWidgets( Canvas canvas, GameObject speechPanel, Button[] choiceButtons ) { } /// Called once before new speech is displayed /// Speech that will be displayed protected virtual void SetupNewSpeech(string speech) { SetSpeechPanelText(speech); ShowSpeechPanel(); } /// Called once after the speech panel has faded out completely protected virtual void TearDownSpeech() { ShowSpeechPanel(false); } /// Called once before a new set of choices is displayed /// Choices that will be displayed protected virtual void SetupNewChoices(string[] choices) { int choiceCount = ArrayHelper.CountNullableArray(choices); for(int index = 0; index < choiceCount; ++index) { if(index < choiceCount) { SetButtonText(index, choices[index]); ShowButton(index); } else { ShowButton(index, false); } } } /// Called once after the choices have been faded out completely protected virtual void TearDownChoices() { if(this.choiceButtons != null) { int buttonCount = this.choiceButtons.Length; for(int index = 0; index < buttonCount; ++index) { ShowButton(index, false); } } } /// Fires the Selected event when the user has made a choice /// Index of the choice the user has picked /// /// If the current dialogue was only speech, the choice index is -1. /// protected virtual void OnSelected(int choiceIndex) { //Debug.Log("Player picked dialogue choice " + choiceIndex); if(Selected != null) { Selected(choiceIndex); } } /// Shows or hides the speech panel /// Whether to show or hide the speech panel protected void ShowSpeechPanel(bool show = true) { if(this.speechPanel != null) { this.speechPanel.gameObject.SetActive(show); } } /// Shows or hides the button with the specified index /// Index of the button that will be enabled /// Whether to show or hide the button protected void ShowButton(int buttonIndex, bool show = true) { if(this.choiceButtons != null) { int buttonCount = this.choiceButtons.Length; if(buttonIndex < buttonCount) { Button button = this.choiceButtons[buttonIndex]; if(button != null) { button.gameObject.SetActive(show); } } } } /// Changes the text in the speech panel /// Text that will be shown in the speech panel protected void SetSpeechPanelText(string text) { if(this.speechText != null) { this.speechText.text = text; } } /// Changes the text on one of the buttons /// Index of the button whose text will be changed /// Text that will appear on the button protected void SetButtonText(int buttonIndex, string text) { if(this.choiceTexts != null) { int buttonCount = this.choiceTexts.Length; if(buttonIndex < buttonCount) { Text textWidget = this.choiceTexts[buttonIndex]; if(textWidget != null) { textWidget.text = text; } } } } /// Looks for the button template widgets in the canvas /// Game object under which the UI widget templates are private void discoverSpeechPanel(GameObject root) { // Look for any panels that have "speech" in their name GameObject speechPanel = null; { Transform parent = root.transform; foreach(Transform child in parent) { if(child.name.Contains("Speech")) { speechPanel = child.gameObject; } } } // If no suitable panel was found, complain to the user so he knows // why the dialogue UI isn't working if(speechPanel == null) { Debug.LogWarning( "No speech panel found. " + "Dialogue canvas '" + gameObject.name + "' will be unable to display speech." ); } else { this.speechPanel = speechPanel; // Look for a 'Text' widget in the speech panel to which we can assign // the dialogue text. this.speechText = this.speechPanel.GetComponentInChildren(); if(speechText == null) { Debug.LogWarning( "No 'Text' widget found in the speech panel. " + "Dialogue canvas '" + gameObject.name + "' will be unable to display speech." ); } } } /// Looks for the button template widgets in the canvas /// Game object under which the UI widget templates are private void discoverButtonWidgets(GameObject root) { // Look up the buttons by which responses can be chosen var choiceButtons = new List